13 Commits
3.0.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
5 changed files with 75 additions and 25 deletions

1
.gitignore vendored
View File

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

View File

@ -13,7 +13,9 @@ license=('GPL3')
makedepends=('git')
depends=('python3'
'rsync'
'python-dotenv')
'python-dotenv'
'python-dbus'
'python-systemd')
install=${pkgname}.install
source=(git+https://github.com/Fuxino/${pkgname}.git)
sha256sums=('SKIP')
@ -27,5 +29,5 @@ pkgver()
package()
{
install -Dm755 "${srcdir}/${pkgname}/${pkgname}.py" "${pkgdir}/usr/bin/${pkgname}"
install -Dm644 "${srcdir}/${pkgname}/${pkgname}.config" "${pkgdir}/etc/${pkgname}/${pkgname}.config"
install -Dm644 "${srcdir}/${pkgname}/${pkgname}.config" "${pkgdir}/etc/${pkgname}/${pkgname}.conf"
}

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

@ -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,11 +8,17 @@ 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
import dbus
from dotenv import load_dotenv
try:
from systemd import journal
except ImportError:
pass
load_dotenv()
@ -29,25 +34,23 @@ 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)
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 decorater_timing(func):
def decorator_timing(func):
@wraps(func)
def wrapper_timing(*args, **kwargs):
start = default_timer()
@ -62,7 +65,7 @@ def timing(_logger):
return wrapper_timing
return decorater_timing
return decorator_timing
class MyFormatter(argparse.RawTextHelpFormatter, argparse.ArgumentDefaultsHelpFormatter):
@ -100,11 +103,18 @@ class Backup:
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
@ -129,6 +139,7 @@ class Backup:
dirs.remove('last_backup')
n_backup = len(dirs) - 1
count = 0
if n_backup > self.keep:
logger.info('Removing old backups...')
@ -137,15 +148,26 @@ class Backup:
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.error('Previous backup could not be read')
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)
@ -162,8 +184,8 @@ class Backup:
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:
@ -192,7 +214,10 @@ 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")}')
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')
@ -211,14 +236,18 @@ class Backup:
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='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')
@ -275,7 +304,25 @@ def simple_backup():
backup = Backup(inputs, output, exclude, keep, backup_options)
if backup.check_params():
backup.run()
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__':