33 Commits
3.0.1 ... 3.2.6

Author SHA1 Message Date
06620a4dba Fix bug when exclude pattern is None 2023-05-31 19:07:50 +02:00
0dd7b887f7 Update PKGBUILD to use release version 2023-05-28 11:07:03 +02:00
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
10 changed files with 234 additions and 82 deletions

2
.gitignore vendored
View File

@ -1 +1,3 @@
.idea/
__pycache__/
test/

View File

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

View File

@ -1,10 +1,47 @@
# simple_backup
simple_backup
============
A simple backup script
## Description
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.
Parameters like input files/directories, output directory etc. can be specified in a configuration file, or on the command line.
Run:
```bash
simple_backup -h
```
to print all possible command line options.
## 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'

View File

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

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,6 +1,6 @@
post_install() {
echo "An example configuration file is found in /etc/simple_backup/simple_backup.config."
echo "Copy it to ~/.simple_backup/simple_backup.config and modify it as needed"
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() {

View File

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

View File

@ -1,7 +1,6 @@
#!/usr/bin/python3
# Import libraries
from dotenv import load_dotenv
import os
from functools import wraps
from shutil import rmtree
@ -9,18 +8,28 @@ import argparse
import configparser
import logging
from logging import StreamHandler
from logging.handlers import RotatingFileHandler
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")
user = os.getenv('SUDO_USER')
homedir = os.path.expanduser(f'~{user}')
else:
homedir = os.getenv('HOME')
@ -29,25 +38,21 @@ logging.getLogger().setLevel(logging.DEBUG)
logger = logging.getLogger(os.path.basename(__file__))
c_handler = StreamHandler()
try:
f_handler = RotatingFileHandler(f'{homedir}/.simple_backup/simple_backup.log', maxBytes=1024000, backupCount=5)
except Exception:
f_handler = None
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 f_handler:
f_handler.setLevel(logging.INFO)
f_format = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
f_handler.setFormatter(f_format)
logger.addHandler(f_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 decorater_timing(func):
def decorator_timing(func):
@wraps(func)
def wrapper_timing(*args, **kwargs):
start = default_timer()
@ -62,7 +67,7 @@ def timing(_logger):
return wrapper_timing
return decorater_timing
return decorator_timing
class MyFormatter(argparse.RawTextHelpFormatter, argparse.ArgumentDefaultsHelpFormatter):
@ -71,12 +76,13 @@ class MyFormatter(argparse.RawTextHelpFormatter, argparse.ArgumentDefaultsHelpFo
class Backup:
def __init__(self, inputs, output, exclude, keep, options):
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 = ''
@ -89,22 +95,18 @@ class 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
@ -120,15 +122,14 @@ class Backup:
def remove_old_backups(self):
try:
dirs = os.listdir(f'{self.output}/simple_backup')
except Exception:
logger.info('No older backups to remove')
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...')
@ -137,33 +138,43 @@ class Backup:
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]}')
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'):
try:
self._last_backup = os.readlink(f'{self.output}/simple_backup/last_backup')
except Exception:
logger.error('Previous backup could not be read')
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()
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
inputs_handle, self._inputs_path = mkstemp(prefix='tmp_inputs', text=True)
exclude_handle, self._exclude_path = mkstemp(prefix='tmp_exclude', text=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:
@ -174,9 +185,13 @@ class Backup:
fp.write('\n')
with open(self._exclude_path, 'w') as fp:
for e in self.exclude:
fp.write(e)
fp.write('\n')
if self.exclude is not None:
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...')
@ -192,15 +207,34 @@ class Backup:
p = Popen(rsync, stdout=PIPE, stderr=STDOUT, shell=True)
output, _ = p.communicate()
logger.info(f'Output of rsync command: {output.decode("utf-8")}')
try:
os.symlink(self._output_dir, f'{self.output}/simple_backup/last_backup')
except Exception:
logger.error('Failed to create last_backup link')
if p.returncode != 0:
self._err_flag = True
if self.keep != -1:
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)
@ -211,14 +245,21 @@ class Backup:
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='A simple backup script written in Python that uses rsync to copy files',
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}/.simple_backup/simple_backup.config',
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')
@ -226,6 +267,8 @@ def _parse_arguments():
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()
@ -251,6 +294,24 @@ def _read_config(config_file):
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)
@ -272,11 +333,15 @@ def simple_backup():
else:
backup_options = '-arvh -H -X'
backup = Backup(inputs, output, exclude, keep, backup_options)
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()