44 Commits
2.0.1 ... 3.2.5

Author SHA1 Message Date
7684bc43b0 Add desktop notifications 2023-05-25 23:44:59 +02:00
a07fce1b6c Version bump 2023-05-25 19:01:50 +02:00
11c1ab0952 Add options to remove old backups before copying files 2023-05-25 19:01:19 +02:00
a42ade4f2b Version bump 2023-05-25 17:27:04 +02:00
c6432c1350 Check rsync return code 2023-05-25 17:20:32 +02:00
fa1d8f410e Remove old last_backup link after copying files 2023-05-25 17:10:38 +02:00
8b9087cdf6 Version bump 2023-05-11 20:21:00 +02:00
3c155e62a2 Version bump 2023-05-11 20:19:56 +02:00
5a4a26f9a7 Fix minor bugs and improve exception handling 2023-05-11 20:18:42 +02:00
ca0e3d133f Fix entry_point in setup.cfg 2023-05-05 19:41:32 +02:00
cc788735dd Fix README.md 2023-05-05 19:30:53 +02:00
631ffa85d3 Remove broken desktop notifications 2023-05-05 19:23:21 +02:00
fe2d66c24c Remove data_files from setup.cfg 2023-05-05 19:12:41 +02:00
97419c30f9 Use setuptools to build the project 2023-05-05 19:03:23 +02:00
c6871ac81a Bump PKGBUILD version 2023-05-04 23:42:55 +02:00
90120031e2 Update PKGBUILD 2023-05-04 23:21:12 +02:00
d61ffa199a Use setuptools 2023-05-04 23:16:15 +02:00
a4c4b88193 Fix PKGBUILD 2023-05-02 18:10:43 +02:00
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
b79e929f2d Improve exception handling 2019-09-18 09:55:46 +02:00
2bcab8f3ab Remove option user
Now the program handles automatically the correct user even when
running with sudo
2019-09-17 19:35:25 +02:00
fbd9520845 Clean code 2019-09-17 16:30:06 +02:00
10 changed files with 466 additions and 609 deletions

2
.gitignore vendored Normal file
View File

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

View File

@ -1,30 +1,48 @@
#Arch Linux PKGBUILD # PKGBUILD
#
#Maintainer: Daniele Fucini <dfucini@gmail.com>
#
pkgname=simple-backup # Maintainer: Daniele Fucini <dfucini@gmail.com>
pkgver=2.0.0.r3.gc61c704
pkgname=simple_backup
pkgver=3.2.3.r1.ga42ade4
pkgrel=1 pkgrel=1
pkgdesc='Simple backup script that uses rsync to copy files' pkgdesc='Simple backup script that uses rsync to copy files'
arch=('any') arch=('any')
url="https://github.com/Fuxino/simple-backup.git" url="https://github.com/Fuxino/simple_backup.git"
license=('GPL3') license=('GPL3')
makedepends=('git') makedepends=('git'
depends=('python3' 'python-setuptools'
'rsync') 'python-build'
'python-installer'
'python-wheel')
depends=('python'
'rsync'
'python-dotenv')
optdepends=('python-systemd: use systemd log'
'python-dbus: for desktop notifications')
install=${pkgname}.install install=${pkgname}.install
source=(git+https://github.com/Fuxino/${pkgname}.git) source=(git+https://github.com/Fuxino/${pkgname}.git)
sha256sums=('SKIP') sha256sums=('SKIP')
pkgver() pkgver()
{ {
cd "$pkgname" cd ${pkgname}
git describe --long --tags | sed 's/\([^-]*-g\)/r\1/;s/-/./g' git describe --long --tags | sed 's/\([^-]*-g\)/r\1/;s/-/./g'
} }
prepare()
{
git -C ${srcdir}/${pkgname} clean -dfx
}
build()
{
cd ${srcdir}/${pkgname}
python -m build --wheel --no-isolation
}
package() package()
{ {
install -Dm755 "${srcdir}/${pkgname}/${pkgname}.py" "${pkgdir}/usr/bin/${pkgname}" cd ${srcdir}/${pkgname}
install -Dm644 "${srcdir}/${pkgname}/config" "${pkgdir}/etc/${pkgname}/config" python -m installer --destdir=${pkgdir} dist/*.whl
install -Dm644 ${srcdir}/${pkgname}/${pkgname}.conf ${pkgdir}/etc/${pkgname}/${pkgname}.conf
} }

View File

@ -1,10 +1,47 @@
# simple-backup simple_backup
============
A simple backup script A simple backup script
## Description ## 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, Parameters like input files/directories, output directory etc. can be specified in a configuration file, or on the command line.
the destination directory for the backup and a few other options. Run:
```bash
simple_backup -h
```
to print all possible command line options.
## Dependencies ## Dependencies
rsync is used to perform the backup. The script uses rsync to actually run the backup, so you will have to install it on your system. For example on Arch Linux:
```bash
sudo pacman -Syu rsync
```
## Install
To install the program, first clone the repository:
```bash
git clone https://github.com/Fuxino/simple_backup.git
```
Install tools required to build and install the package:
```bash
pip install --upgrade build installer wheel
```
Then run:
```bash
cd simple_backup
python -m build --wheel
python -m installer dist/*.whl
```
For Arch Linux, a PKGBUILD that automates this process is provided.
After installing, copy simple_backup.conf (if you used the PKGBUILD on Arch, it will be in /etc/simple_backup/) to $HOME/.config/simple_backup and edit is as needed.

3
pyproject.toml Normal file
View File

@ -0,0 +1,3 @@
[build-system]
requires = ['setuptools']
build-backend = 'setuptools.build_meta'

33
setup.cfg Normal file
View File

@ -0,0 +1,33 @@
[metadata]
name = simple_backup
version = attr: simple_backup.__version__
description = Simple backup script using rsync
long_description = file: README.md
author = Daniele Fucini
author_email = dfucini@gmail.com
license = GPL3
url = https://github.com/Fuxino/simple_backup
classifiers =
Development Status :: 5 - Production/Stable
Environment :: Console
License :: OSI Approved :: GNU General Public License v3 (GPLv3)
Natural Language :: English
Operating System :: POSIX :: Linux
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
Topic :: System :: Archiving :: Backup
[options]
packages = simple_backup
python_requires = >=3.7
install_requires =
python-dotenv
systemd-python
[options.entry_points]
console_scripts =
simple_backup = simple_backup.simple_backup:simple_backup

View File

@ -1,585 +0,0 @@
#!/usr/bin/python3
# Import libraries
from sys import exit, argv
import os
from os.path import expanduser, isfile, isdir, islink, exists
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.user = ''
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('-u, --user USER User performing the backup.')
print(' Useful when running with "sudo"')
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()
move(self.log_path, self.homedir + '/.simple_backup/simple_backup.log')
move(self.err_path, self.homedir + '/.simple_backup/errors.log')
move(self.warn_path, self.homedir + '/.simple_backup/warnings.log')
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()
move(self.log_path, self.homedir + '/.simple_backup/simple_backup.log')
move(self.err_path, self.homedir + '/.simple_backup/errors.log')
move(self.warn_path, self.homedir + '/.simple_backup/warnings.log')
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()
move(self.log_path, self.homedir + '/.simple_backup/simple_backup.log')
move(self.err_path, self.homedir + '/.simple_backup/errors.log')
move(self.warn_path, self.homedir + '/.simple_backup/warnings.log')
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:
print('An error occurred when reading the last_backup link')
exit(1)
else:
self.last_backup = ''
self.backup_dir = self.backup_dir + '/' + date
try:
os.makedirs(self.backup_dir)
except PermissionError as e:
print(str(e))
exit(1)
except:
print('Failed to create backup directory')
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]
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')
if self.user is not None:
pass
# chown $USER:$USER $LOG && chmod 644 $LOG
# chown $USER:$USER $ERR && chmod 644 $ERR
# chown $USER:$USER $WARN && chmod 644 $WARN
self.logfile.close()
self.errfile.close()
self.warnfile.close()
move(self.log_path, self.homedir + '/.simple_backup/simple_backup.log')
move(self.err_path, self.homedir + '/.simple_backup/errors.log')
move(self.warn_path, self.homedir + '/.simple_backup/warnings.log')
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:
print('An error occurred when reading the last_backup link')
exit(1)
else:
self.last_backup = ''
self.backup_dir = self.backup_dir + '/' + date
try:
os.makedirs(self.backup_dir)
except PermissionError as e:
print(str(e))
exit(1)
except:
print('Failed to create backup directory')
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 == '-u' or var == '--user':
self.homedir = '/home/' + argv[i+1]
if not isdir(self.homedir):
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: user "' + argv[i+1] + '"doesn\'t exist'
self.errfile.write(err_message)
self.errfile.write('\n')
if self.user is not None:
pass
# chown $USER:$USER $LOG && chmod 644 $LOG
# chown $USER:$USER $ERR && chmod 644 $ERR
# chown $USER:$USER $WARN && chmod 644 $WARN
self.logfile.close()
self.errfile.close()
self.warnfile.close()
move(self.log_path, self.homedir + '/.simple_backup/simple_backup.log')
move(self.err_path, self.homedir + '/.simple_backup/errors.log')
move(self.warn_path, self.homedir + '/.simple_backup/warnings.log')
exit(1)
if not isdir(self.homedir + '/.simple_backup'):
try:
os.makedir(self.homedir + '/.simple_backup')
print('Created directory "' + self.homedir + '/.simple_backup".')
except:
print('Failed to create .simple_backup directory in HOME')
exit(1)
self.user = 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'
self.errfile.write(err_message)
self.errfile.write('\n')
self.logfile.close()
self.errfile.close()
self.warnfile.close()
move(self.log_path, self.homedir + '/.simple_backup/simple_backup.log')
move(self.err_path, self.homedir + '/.simple_backup/errors.log')
move(self.warn_path, self.homedir + '/.simple_backup/warnings.log')
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
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')
print('Created directory "' + backup.homedir + '/.simple_backup".')
print('Copy there the sample configuration and edit it')
print('to your needs before running the backup,')
print('or pass options on the command line.')
except:
print('Failed to create .simple_backup directory in HOME')
exit(1)
# Read configuration file
backup.read_conf()
else:
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')
err_message = 'Error: Output folder "' + backup.backup_dev.getBackupDev + '" not found'
backup.errfile.write(err_message)
backup.errfile.write('\n')
backup.logfile.close()
backup.errfile.close()
backup.warnfile.close()
move(backup.log_path, backup.homedir + '/.simple_backup/simple_backup.log')
move(backup.err_path, backup.homedir + '/.simple_backup/errors.log')
move(backup.warn_path, backup.homedir + '/.simple_backup/warnings.log')
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')
print('Created directory "' + backup.homedir + '/.simple_backup".')
print('Copy there the sample configuration and edit it')
print('to your needs before running the backup')
print('or pass options on the command line.')
except:
print('Failed to create .simple_backup directory in HOME')
exit(1)
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'
backup.warnfile.write(warn_message)
backup.warnfile.write('\n')
print(warn_message)
backup.logfile.close()
backup.errfile.close()
backup.warnfile.close()
move(backup.log_path, backup.homedir + '/.simple_backup/simple_backup.log')
move(backup.err_path, backup.homedir + '/.simple_backup/errors.log')
move(backup.warn_path, backup.homedir + '/.simple_backup/warnings.log')
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:
print('Failed to list backup directory')
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()
err_flag = 0
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')
err_flag = 1
if err_flag:
print(err_message)
backup.logfile.close()
backup.errfile.close()
backup.warnfile.close()
backup.exec_()
# Update the logs
backup.logfile = open(backup.log_path, 'a')
if os.stat(backup.err_path).st_size > 0:
log_message = str(datetime.now()) + ': Backup finished with errors'
backup.logfile.write(log_message)
backup.logfile.write('\n')
print('Backup finished with errors')
elif os.stat(backup.warn_path).st_size > 0:
log_message = str(datetime.now()) + ': Backup finished with warnings'
backup.logfile.write(log_message)
backup.logfile.write('\n')
print('Backup finished (warnings)')
else:
log_message = str(datetime.now()) + ': Backup finished'
backup.logfile.write(log_message)
backup.logfile.write('\n')
print('Backup finished')
backup.logfile.close()
# Copy log files in home directory
move(backup.log_path, backup.homedir + '/.simple_backup/simple_backup.log')
move(backup.err_path, backup.homedir + '/.simple_backup/errors.log')
move(backup.warn_path, backup.homedir + '/.simple_backup/warnings.log')
if islink(backup.backup_dev + '/simple_backup/last_backup'):
try:
os.remove(backup.backup_dev + '/simple_backup/last_backup')
except:
print('Failed to remove last_backup link')
try:
os.symlink(backup.backup_dir, backup.backup_dev + '/simple_backup/last_backup')
except:
print('Failed to create last_backup link')
# 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 #Example config file for simple_backup
# WARNING: Values should NOT be quoted, e.g. use inputs=/some/dir instead of inputs="/some/dir"
[default]
#Input directories. Use a comma to separate items #Input directories. Use a comma to separate items
inputs=/home/my_home,/etc inputs=/home/my_home,/etc
@ -9,7 +8,7 @@ inputs=/home/my_home,/etc
backup_dir=/media/Backup backup_dir=/media/Backup
#Exclude patterns. Use a comma to separate items #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) #Number of snapshots to keep (use -1 to keep all)
keep=-1 keep=-1

View File

@ -1,5 +1,6 @@
post_install() { 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() { post_upgrade() {

View File

@ -0,0 +1,3 @@
"""Init."""
__version__ = '3.2.5'

346
simple_backup/simple_backup.py Executable file
View File

@ -0,0 +1,346 @@
#!/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
from dotenv import load_dotenv
try:
from systemd import journal
except ImportError:
journal = None
try:
import dbus
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)
if journal:
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)
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, remove_before=False):
self.inputs = inputs
self.output = output
self.exclude = exclude
self.options = options
self.keep = keep
self.remove_before = remove_before
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
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 FileNotFoundError:
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 FileNotFoundError:
logger.error(f'Error while removing backup {dirs[i]}. Directory not found')
except PermissionError:
logger.error(f'Error while removing backup {dirs[i]}. Permission denied')
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'):
link = os.readlink(f'{self.output}/simple_backup/last_backup')
if os.path.isdir(link):
self._last_backup = link
else:
logger.info('No previous backups available')
else:
logger.info('No previous backups available')
# Function to read configuration file
@timing(logger)
def run(self):
logger.info('Starting backup...')
try:
notify('Starting backup...')
except NameError:
pass
self.create_backup_dir()
self.find_last_backup()
_, 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')
if self.keep != -1 and self.remove_before:
self.remove_old_backups()
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()
if p.returncode != 0:
self._err_flag = True
output = output.decode("utf-8").split('\n')
logger.info(f'rsync: {output[-3]}')
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:
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)')
try:
notify('Backup finished with errors (check log for details)')
except NameError:
pass
else:
notify('Backup finished')
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)')
parser.add_argument('--remove-before-backup', action='store_true',
help='Remove old backups before executing the backup, instead of after')
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 notify(text):
euid = os.geteuid()
if euid == 0:
uid = os.getenv('SUDO_UID')
else:
uid = os.geteuid()
os.seteuid(int(uid))
os.environ['DBUS_SESSION_BUS_ADDRESS'] = f'unix:path=/run/user/{uid}/bus'
obj = dbus.SessionBus().get_object('org.freedesktop.Notifications', '/org/freedesktop/Notifications')
obj = dbus.Interface(obj, 'org.freedesktop.Notifications')
obj.Notify('', 0, '', 'simple_backup', text, [], {'urgency': 1}, 10000)
os.seteuid(int(euid))
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, remove_before=args.remove_before_backup)
if backup.check_params():
backup.run()
return 0
return 1
if __name__ == '__main__':
simple_backup()