12 Commits
3.0.0 ... 3.1.0

Author SHA1 Message Date
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
4 changed files with 97 additions and 41 deletions

1
.gitignore vendored
View File

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

View File

@ -13,7 +13,8 @@ license=('GPL3')
makedepends=('git') makedepends=('git')
depends=('python3' depends=('python3'
'rsync' 'rsync'
'python-dotenv') 'python-dotenv'
'python-dbus')
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')
@ -27,5 +28,5 @@ pkgver()
package() package()
{ {
install -Dm755 "${srcdir}/${pkgname}/${pkgname}.py" "${pkgdir}/usr/bin/${pkgname}" 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}.config"
} }

View File

@ -1,6 +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.config."
echo "Copy it to ~/.simple_backup/simple_backup.config and modify it as needed" echo "Copy it to ~/config/simple_backup/simple_backup.config and modify it as needed"
} }
post_upgrade() { post_upgrade() {

View File

@ -1,17 +1,71 @@
#!/usr/bin/python3 #!/usr/bin/python3
# Import libraries # Import libraries
from dotenv import load_dotenv
import os import os
from functools import wraps
from shutil import rmtree from shutil import rmtree
import argparse import argparse
import configparser import configparser
import logging import logging
from logging import StreamHandler from logging import StreamHandler
from logging.handlers import RotatingFileHandler 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
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): class MyFormatter(argparse.RawTextHelpFormatter, argparse.ArgumentDefaultsHelpFormatter):
@ -47,7 +101,7 @@ class Backup:
self.inputs.append(i_new) self.inputs.append(i_new)
except Exception: except Exception:
logger.warning(f'Input {i} is a link and cannot be read. Skipping') logger.warning(f'Input {i} is a link and cannot be read. Skipping')
self.inpts.remove(i) self.inputs.remove(i)
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')
@ -78,6 +132,7 @@ class Backup:
dirs.remove('last_backup') dirs.remove('last_backup')
n_backup = len(dirs) - 1 n_backup = len(dirs) - 1
count = 0
if n_backup > self.keep: if n_backup > self.keep:
logger.info('Removing old backups...') logger.info('Removing old backups...')
@ -86,17 +141,29 @@ class Backup:
for i in range(n_backup - self.keep): for i in range(n_backup - self.keep):
try: try:
rmtree(f'{self.output}/simple_backup/{dirs[i]}') rmtree(f'{self.output}/simple_backup/{dirs[i]}')
count += 1
except Exception: except Exception:
logger.error(f'Error while removing backup {dirs[i]}') 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): def find_last_backup(self):
if os.path.islink(f'{self.output}/simple_backup/last_backup'): if os.path.islink(f'{self.output}/simple_backup/last_backup'):
try: try:
self._last_backup = os.readlink(f'{self.output}/simple_backup/last_backup') self._last_backup = os.readlink(f'{self.output}/simple_backup/last_backup')
except Exception: 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 # Function to read configuration file
@timing(logger)
def run(self): def run(self):
logger.info('Starting backup...') logger.info('Starting backup...')
@ -110,8 +177,8 @@ class Backup:
logger.error('Failed to remove last_backup link') logger.error('Failed to remove last_backup link')
self._err_flag = True self._err_flag = True
inputs_handle, self._inputs_path = mkstemp(prefix='tmp_inputs', text=True) _, self._inputs_path = mkstemp(prefix='tmp_inputs', text=True)
exclude_handle, self._exclude_path = mkstemp(prefix='tmp_exclude', text=True) _, self._exclude_path = mkstemp(prefix='tmp_exclude', text=True)
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:
@ -140,7 +207,10 @@ class Backup:
p = Popen(rsync, stdout=PIPE, stderr=STDOUT, shell=True) p = Popen(rsync, stdout=PIPE, stderr=STDOUT, shell=True)
output, _ = p.communicate() 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: try:
os.symlink(self._output_dir, f'{self.output}/simple_backup/last_backup') os.symlink(self._output_dir, f'{self.output}/simple_backup/last_backup')
@ -159,35 +229,9 @@ class Backup:
if self._err_flag: if self._err_flag:
logger.warning('Some errors occurred (check log for details)') logger.warning('Some errors occurred (check log for details)')
return 1
load_dotenv() return 0
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()
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)
def _parse_arguments(): def _parse_arguments():
@ -196,7 +240,7 @@ def _parse_arguments():
epilog='Report bugs to dfucini<at>gmail<dot>com', epilog='Report bugs to dfucini<at>gmail<dot>com',
formatter_class=MyFormatter) 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.config',
help='Specify location of configuration file') help='Specify location of configuration file')
parser.add_argument('-i', '--input', nargs='+', help='Paths/files to backup') 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('-o', '--output', help='Output directory for the backup')
@ -250,10 +294,20 @@ def simple_backup():
else: else:
backup_options = '-arvh -H -X' backup_options = '-arvh -H -X'
output = os.path.abspath(output)
backup = Backup(inputs, output, exclude, keep, backup_options) backup = Backup(inputs, output, exclude, keep, backup_options)
if backup.check_params(): if backup.check_params():
backup.run() 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)
status = backup.run()
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)
if __name__ == '__main__': if __name__ == '__main__':