Compare commits

...
3 Commits
Author SHA1 Message Date
Fuxino fdb2fc960a Bump version to 4.6.1 2026-11-02 20:18:52 +01:00
Fuxino c5cfe49758 Show elapsed time as (h:)mm:ss and add it to the notification
Replace the timing decorator with a start time kept by Backup.run(), so the final
notification can report how long the backup took.
2026-11-02 19:54:11 +01:00
Fuxino 8d0fd46f44 Update PKGBUILD 2026-10-25 12:42:54 +01:00
5 changed files with 41 additions and 30 deletions
+2 -2
View File
@@ -4,7 +4,7 @@
pkgname=simple_backup
pkgdesc='Simple backup script that uses rsync to copy files'
pkgver=4.5.0
pkgver=4.6.0
pkgrel=1
url="https://git.shouldnt.work/fuxino/$pkgname"
arch=('any')
@@ -24,7 +24,7 @@ checkdepends=('python-pytest'
conflicts=('simple_backup-git')
source=("git+$url?signed#tag=$pkgver")
validpgpkeys=('7E12BC1FF3B6EDB2CD8053EB981A2B2A3BBF5514')
sha256sums=('c0f7c38d99208e1327c0a6af1d58d5d13797d1cef54f99e1b2fcb4f4a9480609')
sha256sums=('049c900b2d778434baa579409ccbb2b66ddb0968cf1dd23b3de0eda65ef3d2c6')
build()
{
+1 -1
View File
@@ -1,4 +1,4 @@
.TH SIMPLE_BACKUP 1 2026-10-25 SIMPLE_BACKUP 4.6.0
.TH SIMPLE_BACKUP 1 2026-11-02 SIMPLE_BACKUP 4.6.1
.SH NAME
simple_backup \- Backup files and folders using rsync
.SH SYNOPSIS
+1 -1
View File
@@ -1,3 +1,3 @@
"""Init."""
__version__ = '4.6.0'
__version__ = '4.6.1'
+18 -26
View File
@@ -25,9 +25,8 @@ import shlex
import sys
import warnings
from collections import deque
from collections.abc import Callable, Iterator
from collections.abc import Iterator
from datetime import datetime
from functools import wraps
from getpass import GetPassWarning, getpass
from glob import glob
from logging import StreamHandler
@@ -35,7 +34,7 @@ from shutil import get_terminal_size, rmtree, which
from subprocess import PIPE, STDOUT, Popen
from tempfile import mkstemp
from timeit import default_timer
from typing import Any, ParamSpec, TypedDict, TypeVar
from typing import Any, TypedDict
try:
import paramiko
@@ -69,9 +68,6 @@ if journal:
j_handler.setFormatter(j_format)
logger.addHandler(j_handler)
P = ParamSpec('P')
R = TypeVar('R')
# Seconds to wait for the TCP connection to the SSH server
SSH_TIMEOUT = 15
@@ -80,25 +76,15 @@ PROGRESS_RE = re.compile(r'\s*(\S+)\s+\d+%\s+\S+\s+\d+:\d\d:\d\d(?:\s+\(xfr#\d+,
PROGRESS_BAR_WIDTH = 30
def timing(func: Callable[P, R]) -> Callable[P, R]:
"""Decorator to measure execution time of a function
def _format_duration(seconds: float) -> str:
"""Format seconds as mm:ss, or h:mm:ss from one hour"""
Parameters:
func: Function to decorate
"""
@wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
start = default_timer()
s = round(seconds)
value = func(*args, **kwargs)
if s >= 3600:
return f'{s // 3600}:{s // 60 % 60:02d}:{s % 60:02d}'
end = default_timer()
logger.info('Elapsed time: %.3f seconds', end - start)
return value
return wrapper
return f'{s // 60:02d}:{s % 60:02d}'
class MyFormatter(argparse.RawTextHelpFormatter, argparse.ArgumentDefaultsHelpFormatter):
@@ -233,6 +219,7 @@ class Backup:
# Passed to sshpass through rsync's environment only
self._password: str | None = None
self._removed_count = 0
self._start_time = 0.0
def check_params(self, homedir: str = '') -> int:
"""Check if parameters for the backup are valid"""
@@ -669,14 +656,19 @@ class Backup:
'Rsync error (return code %d) - Check rsync(1) for details', returncode)
# Function to read configuration file
@timing
def run(self) -> int:
"""Perform the backup"""
self._start_time = default_timer()
try:
return self._run()
finally:
self._remove_temp_files()
logger.info('Elapsed time: %s', self._elapsed())
def _elapsed(self) -> str:
return _format_duration(default_timer() - self._start_time)
def close(self) -> None:
"""Close the SSH connection, if open"""
@@ -831,14 +823,14 @@ class Backup:
logger.error(
'Some errors occurred while performing the backup')
_notify(
'Some errors occurred while performing the backup. Check log for details')
_notify(f'Some errors occurred while performing the backup (elapsed time {self._elapsed()}). '
'Check log for details')
return 4
logger.info('Backup completed')
_notify('Backup completed')
_notify(f'Backup completed in {self._elapsed()}')
return 0
+19
View File
@@ -88,6 +88,18 @@ class TestRsync:
assert 'Backup completed' in caplog.text
assert 'rsync: sent' in caplog.text
def test_elapsed_time(self, src: Path, out: Path, monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture) -> None:
notifications: list[str] = []
monkeypatch.setattr(sb, '_notify', notifications.append)
# Start, then the notification and the log line
monkeypatch.setattr(sb, 'default_timer', iter([0.0, 863.187, 863.2]).__next__)
assert run_backup(out, [str(src)]) == 0
assert notifications[-1] == 'Backup completed in 14:23'
assert 'Elapsed time: 14:23' in caplog.text
def test_hard_links(self, src: Path, out: Path) -> None:
assert run_backup(out, [str(src)]) == 0
(src / 'changed.txt').write_text('second')
@@ -231,6 +243,13 @@ class TestReadRecords:
assert list(sb._read_records(stream)) == ['file', '', 'namé', '�']
@pytest.mark.parametrize(('seconds', 'text'), [
(0.4, '00:00'), (59.6, '01:00'), (863.187, '14:23'), (3599.4, '59:59'), (3600, '1:00:00'), (37230, '10:20:30'),
])
def test_format_duration(seconds: float, text: str) -> None:
assert sb._format_duration(seconds) == text
class TestProgressBar:
@pytest.fixture(autouse=True)
def terminal(self, monkeypatch: pytest.MonkeyPatch) -> None: