23 Commits
2.1.1 ... 3.1.2

Author SHA1 Message Date
eb8bdde1fc Rename configuration file 2023-05-02 18:07:41 +02:00
5d17aaf03a Update dependencies in PKGBUILD 2023-04-30 22:06:30 +02:00
d181d2970f Better handle missing parameters 2023-04-30 14:38:26 +02:00
84e6d58493 Handle dbus exceptions 2023-04-30 11:53:31 +02:00
d2e982469b Log number of old backups removed 2023-04-30 11:41:37 +02:00
0dde6b8df9 Fix last_backup broken link detection 2023-04-30 11:32:35 +02:00
e45ae1b1b6 Decrease log verbosity 2023-04-30 11:02:59 +02:00
d1ccc33d0a Update gitignore 2023-04-30 10:41:51 +02:00
8b52115b4a Change journal log format 2023-04-30 10:40:18 +02:00
c02fd658bc Use systemd journal for logging 2023-04-29 22:26:38 +02:00
7d0125344f Update install file 2023-04-29 21:53:06 +02:00
3f2e133eff Change default config path 2023-04-29 21:50:35 +02:00
d2e03f89ca Add desktop notifications 2023-04-29 21:28:42 +02:00
92ecc95f72 Fix Arch Linux .install file 2023-04-15 22:16:50 +02:00
3e5e3cb066 Add timing 2023-04-15 21:53:16 +02:00
bd74cf0e14 Fix PKGBUILD 2023-04-15 21:43:09 +02:00
1a251f61af Send rsync output to logger 2023-04-15 21:36:45 +02:00
661a5d4232 Add requirements.txt 2023-04-15 21:06:58 +02:00
642d807d61 Use destination if input is symlink 2023-04-15 21:01:01 +02:00
b08b0474ac Refactor code 2023-04-15 19:49:33 +02:00
b4dee0ccd2 Rename config file 2023-04-15 12:40:09 +02:00
da07c602b5 Fix small bugs 2023-04-15 11:09:36 +02:00
b4294a5792 Add .gitignore 2023-04-15 10:39:26 +02:00
8 changed files with 347 additions and 868 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
.idea/
test/

View File

@ -3,16 +3,19 @@
#Maintainer: Daniele Fucini <dfucini@gmail.com>
#
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'
'rsync')
'rsync'
'python-dotenv'
'python-dbus'
'python-systemd')
install=${pkgname}.install
source=(git+https://github.com/Fuxino/${pkgname}.git)
sha256sums=('SKIP')
@ -26,5 +29,5 @@ pkgver()
package()
{
install -Dm755 "${srcdir}/${pkgname}/${pkgname}.py" "${pkgdir}/usr/bin/${pkgname}"
install -Dm644 "${srcdir}/${pkgname}/config" "${pkgdir}/etc/${pkgname}/config"
install -Dm644 "${srcdir}/${pkgname}/${pkgname}.config" "${pkgdir}/etc/${pkgname}/${pkgname}.conf"
}

View File

@ -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.

1
requirements.txt Normal file
View File

@ -0,0 +1 @@
python-dotenv>=1.0.0

View File

@ -1,856 +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
def help_function(self):
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:
print('Failed to create logs in {}'.format(self.homedir))
try:
os.remove(self.warn_path)
except:
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:
print('Failed to create logs in {}'.format(self.homedir))
try:
os.remove(self.warn_path)
except:
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(',')
n_in = 0
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:
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:
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:
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:
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:
print('Failed to remove temporary files')
exit(1)
except:
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:
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:
print('Failed to remove temporary files')
exit(1)
return
# 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 == '-h' or var == '--help':
self.help_function()
elif var == '-i' or var == '--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 == '-d' or var == '--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:
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:
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:
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:
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:
print('Failed to remove temporary files')
exit(1)
except:
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:
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:
print('Failed to remove temporary files')
exit(1)
i = i + 1
elif var == '-e' or var == '--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 == '-k' or var == '--keep':
self.keep = int(argv[i+1])
i = i + 1
elif var == '-c' or var == '--config':
self.read_conf(argv[i+1])
i = i + 1
elif var == '-s' or var == '--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:
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:
print('Failed to remove temporary files')
exit(1)
i = i + 1
self.inputs.close()
self.exclude.close()
return
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)
return
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:
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.makedir(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:
print('Failed to create logs in {}'.format(backup.homedir))
try:
os.remove(backup.err_path)
os.remove(backup.warn_path)
except:
print('Failed to remove temporary files')
except:
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:
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.getBackupDev + '" 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:
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:
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.makedir(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:
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:
print('Failed to remove temporary files')
except:
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:
print('Failed to remove temporary files')
exit(1)
try:
os.remove(backup.inputs_path)
os.remove(backup.exclude_path)
except:
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:
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:
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:
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:
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:
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:
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:
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:
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:
print('Failed to create logs in {}'.format(backup.homedir))
try:
os.remove(backup.warn_path)
except:
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:
print('Failed to create logs in {}'.format(backup.homedir))
try:
os.remove(backup.err_path)
except:
print('Failed to remove temporary file')
if isfile(backup.homedir + '/.simple_backup/errors.log'):
try:
os.remove(backup.homedir + '/.simple_backup/errors.log')
except:
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:
print('Failed to remove temporary files')
if isfile(backup.homedir + '/.simple_backup/errors.log'):
try:
os.remove(backup.homedir + '/.simple_backup/errors.log')
except:
print('Failed to remove old logs')
if isfile(backup.homedir + '/.simple_backup/warnings.log'):
try:
os.remove(backup.homedir + '/.simple_backup/warnings.log')
except:
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:
print('Failed to create logs in {}'.format(backup.homedir))
# Delete temporary files
try:
os.remove(backup.inputs_path)
os.remove(backup.exclude_path)
except:
print('Failed to remove temporary files')
exit(0)
if __name__ == '__main__':
main()

View File

@ -1,7 +1,6 @@
#Example config file for my_backup
# WARNING: Values should NOT be quoted, e.g. use inputs=/some/dir instead of inputs="/some/dir"
#Example config file for simple_backup
[default]
#Input directories. Use a comma to separate items
inputs=/home/my_home,/etc
@ -9,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
#Number of snapshots to keep (use -1 to keep all)
keep=-1
keep=-1

View File

@ -1,5 +1,6 @@
post_install() {
echo "An example configuration file is found in /etc/simple_backup/config."
echo "An example configuration file is found in /etc/simple_backup/simple_backup.conf."
echo "Copy it to ~/config/simple_backup/simple_backup.conf and modify it as needed"
}
post_upgrade() {

329
simple_backup.py Executable file
View File

@ -0,0 +1,329 @@
#!/usr/bin/python3
# Import libraries
import os
from functools import wraps
from shutil import rmtree
import argparse
import configparser
import logging
from logging import StreamHandler
from timeit import default_timer
from subprocess import Popen, PIPE, STDOUT
from datetime import datetime
from tempfile import mkstemp
import dbus
from dotenv import load_dotenv
try:
from systemd import journal
except ImportError:
pass
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.getLogger().setLevel(logging.DEBUG)
logger = logging.getLogger(os.path.basename(__file__))
c_handler = StreamHandler()
c_handler.setLevel(logging.INFO)
c_format = logging.Formatter('%(name)s - %(levelname)s - %(message)s')
c_handler.setFormatter(c_format)
logger.addHandler(c_handler)
try:
j_handler = journal.JournalHandler()
j_handler.setLevel(logging.INFO)
j_format = logging.Formatter('%(levelname)s - %(message)s')
j_handler.setFormatter(j_format)
logger.addHandler(j_handler)
except NameError:
pass
def timing(_logger):
def decorator_timing(func):
@wraps(func)
def wrapper_timing(*args, **kwargs):
start = default_timer()
value = func(*args, **kwargs)
end = default_timer()
_logger.info(f'Elapsed time: {end - start:.3f} seconds')
return value
return wrapper_timing
return decorator_timing
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 = ''
self._err_flag = False
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
for i in self.inputs:
if os.path.islink(i):
try:
i_new = os.readlink(i)
logger.info(f'Input {i} is a symbolic link referencing {i_new}. Copying {i_new} instead')
self.inputs.remove(i)
self.inputs.append(i_new)
except Exception:
logger.warning(f'Input {i} is a link and cannot be read. Skipping')
self.inputs.remove(i)
if self.output is None:
logger.critical('No output path specified. Use -o argument or specify output path in configuration file')
return False
if not os.path.isdir(self.output):
logger.critical('Output path for backup does not exist')
return False
self.output = os.path.abspath(self.output)
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
count = 0
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]}')
count += 1
except Exception:
logger.error(f'Error while removing backup {dirs[i]}')
if count == 1:
logger.info(f'Removed {count} backup')
else:
logger.info(f'Removed {count} backups')
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('Previous backup could not be read')
else:
logger.info('No previous backups available')
if not os.path.isdir(self._last_backup):
logger.warning('Previous backup could not be read')
# Function to read configuration file
@timing(logger)
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')
self._err_flag = True
_, self._inputs_path = mkstemp(prefix='tmp_inputs', text=True)
_, 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'
p = Popen(rsync, stdout=PIPE, stderr=STDOUT, shell=True)
output, _ = p.communicate()
output = output.decode("utf-8").split('\n')
logger.info(f'rsync: {output[-3]}')
logger.info(f'rsync: {output[-2]}')
try:
os.symlink(self._output_dir, f'{self.output}/simple_backup/last_backup')
except Exception:
logger.error('Failed to create last_backup link')
self._err_flag = True
if self.keep != -1:
self.remove_old_backups()
os.remove(self._inputs_path)
os.remove(self._exclude_path)
logger.info('Backup completed')
if self._err_flag:
logger.warning('Some errors occurred (check log for details)')
return 1
return 0
def _parse_arguments():
parser = argparse.ArgumentParser(prog='simple_backup',
description='Simple backup script written in Python that uses rsync to copy files',
epilog='Report bugs to dfucini<at>gmail<dot>com',
formatter_class=MyFormatter)
parser.add_argument('-c', '--config', default=f'{homedir}/.config/simple_backup/simple_backup.conf',
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():
try:
obj = dbus.SessionBus().get_object("org.freedesktop.Notifications", "/org/freedesktop/Notifications")
obj = dbus.Interface(obj, "org.freedesktop.Notifications")
obj.Notify("simple_backup", 0, "", "Starting backup...", "", [], {"urgency": 1}, 10000)
except dbus.exceptions.DBusException:
obj = None
status = backup.run()
if obj is not None:
if status == 0:
obj.Notify("simple_backup", 0, "", "Backup finished.", "", [], {"urgency": 1}, 10000)
else:
obj.Notify("simple_backup", 0, "", "Backup finished. Some errors occurred.",
"", [], {"urgency": 1}, 10000)
return 0
return 1
if __name__ == '__main__':
simple_backup()