Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdb2fc960a
|
||
|
|
c5cfe49758
|
||
|
|
8d0fd46f44
|
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
pkgname=simple_backup
|
pkgname=simple_backup
|
||||||
pkgdesc='Simple backup script that uses rsync to copy files'
|
pkgdesc='Simple backup script that uses rsync to copy files'
|
||||||
pkgver=4.5.0
|
pkgver=4.6.0
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
url="https://git.shouldnt.work/fuxino/$pkgname"
|
url="https://git.shouldnt.work/fuxino/$pkgname"
|
||||||
arch=('any')
|
arch=('any')
|
||||||
@@ -24,7 +24,7 @@ checkdepends=('python-pytest'
|
|||||||
conflicts=('simple_backup-git')
|
conflicts=('simple_backup-git')
|
||||||
source=("git+$url?signed#tag=$pkgver")
|
source=("git+$url?signed#tag=$pkgver")
|
||||||
validpgpkeys=('7E12BC1FF3B6EDB2CD8053EB981A2B2A3BBF5514')
|
validpgpkeys=('7E12BC1FF3B6EDB2CD8053EB981A2B2A3BBF5514')
|
||||||
sha256sums=('c0f7c38d99208e1327c0a6af1d58d5d13797d1cef54f99e1b2fcb4f4a9480609')
|
sha256sums=('049c900b2d778434baa579409ccbb2b66ddb0968cf1dd23b3de0eda65ef3d2c6')
|
||||||
|
|
||||||
build()
|
build()
|
||||||
{
|
{
|
||||||
|
|||||||
+1
-1
@@ -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
|
.SH NAME
|
||||||
simple_backup \- Backup files and folders using rsync
|
simple_backup \- Backup files and folders using rsync
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
"""Init."""
|
"""Init."""
|
||||||
|
|
||||||
__version__ = '4.6.0'
|
__version__ = '4.6.1'
|
||||||
|
|||||||
@@ -25,9 +25,8 @@ import shlex
|
|||||||
import sys
|
import sys
|
||||||
import warnings
|
import warnings
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from collections.abc import Callable, Iterator
|
from collections.abc import Iterator
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from functools import wraps
|
|
||||||
from getpass import GetPassWarning, getpass
|
from getpass import GetPassWarning, getpass
|
||||||
from glob import glob
|
from glob import glob
|
||||||
from logging import StreamHandler
|
from logging import StreamHandler
|
||||||
@@ -35,7 +34,7 @@ from shutil import get_terminal_size, rmtree, which
|
|||||||
from subprocess import PIPE, STDOUT, Popen
|
from subprocess import PIPE, STDOUT, Popen
|
||||||
from tempfile import mkstemp
|
from tempfile import mkstemp
|
||||||
from timeit import default_timer
|
from timeit import default_timer
|
||||||
from typing import Any, ParamSpec, TypedDict, TypeVar
|
from typing import Any, TypedDict
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import paramiko
|
import paramiko
|
||||||
@@ -69,9 +68,6 @@ if journal:
|
|||||||
j_handler.setFormatter(j_format)
|
j_handler.setFormatter(j_format)
|
||||||
logger.addHandler(j_handler)
|
logger.addHandler(j_handler)
|
||||||
|
|
||||||
P = ParamSpec('P')
|
|
||||||
R = TypeVar('R')
|
|
||||||
|
|
||||||
# Seconds to wait for the TCP connection to the SSH server
|
# Seconds to wait for the TCP connection to the SSH server
|
||||||
SSH_TIMEOUT = 15
|
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
|
PROGRESS_BAR_WIDTH = 30
|
||||||
|
|
||||||
|
|
||||||
def timing(func: Callable[P, R]) -> Callable[P, R]:
|
def _format_duration(seconds: float) -> str:
|
||||||
"""Decorator to measure execution time of a function
|
"""Format seconds as mm:ss, or h:mm:ss from one hour"""
|
||||||
|
|
||||||
Parameters:
|
s = round(seconds)
|
||||||
func: Function to decorate
|
|
||||||
"""
|
|
||||||
@wraps(func)
|
|
||||||
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
||||||
start = default_timer()
|
|
||||||
|
|
||||||
value = func(*args, **kwargs)
|
if s >= 3600:
|
||||||
|
return f'{s // 3600}:{s // 60 % 60:02d}:{s % 60:02d}'
|
||||||
|
|
||||||
end = default_timer()
|
return f'{s // 60:02d}:{s % 60:02d}'
|
||||||
|
|
||||||
logger.info('Elapsed time: %.3f seconds', end - start)
|
|
||||||
|
|
||||||
return value
|
|
||||||
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
|
||||||
class MyFormatter(argparse.RawTextHelpFormatter, argparse.ArgumentDefaultsHelpFormatter):
|
class MyFormatter(argparse.RawTextHelpFormatter, argparse.ArgumentDefaultsHelpFormatter):
|
||||||
@@ -233,6 +219,7 @@ class Backup:
|
|||||||
# Passed to sshpass through rsync's environment only
|
# Passed to sshpass through rsync's environment only
|
||||||
self._password: str | None = None
|
self._password: str | None = None
|
||||||
self._removed_count = 0
|
self._removed_count = 0
|
||||||
|
self._start_time = 0.0
|
||||||
|
|
||||||
def check_params(self, homedir: str = '') -> int:
|
def check_params(self, homedir: str = '') -> int:
|
||||||
"""Check if parameters for the backup are valid"""
|
"""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)
|
'Rsync error (return code %d) - Check rsync(1) for details', returncode)
|
||||||
|
|
||||||
# Function to read configuration file
|
# Function to read configuration file
|
||||||
@timing
|
|
||||||
def run(self) -> int:
|
def run(self) -> int:
|
||||||
"""Perform the backup"""
|
"""Perform the backup"""
|
||||||
|
|
||||||
|
self._start_time = default_timer()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return self._run()
|
return self._run()
|
||||||
finally:
|
finally:
|
||||||
self._remove_temp_files()
|
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:
|
def close(self) -> None:
|
||||||
"""Close the SSH connection, if open"""
|
"""Close the SSH connection, if open"""
|
||||||
@@ -831,14 +823,14 @@ class Backup:
|
|||||||
logger.error(
|
logger.error(
|
||||||
'Some errors occurred while performing the backup')
|
'Some errors occurred while performing the backup')
|
||||||
|
|
||||||
_notify(
|
_notify(f'Some errors occurred while performing the backup (elapsed time {self._elapsed()}). '
|
||||||
'Some errors occurred while performing the backup. Check log for details')
|
'Check log for details')
|
||||||
|
|
||||||
return 4
|
return 4
|
||||||
|
|
||||||
logger.info('Backup completed')
|
logger.info('Backup completed')
|
||||||
|
|
||||||
_notify('Backup completed')
|
_notify(f'Backup completed in {self._elapsed()}')
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|||||||
@@ -88,6 +88,18 @@ class TestRsync:
|
|||||||
assert 'Backup completed' in caplog.text
|
assert 'Backup completed' in caplog.text
|
||||||
assert 'rsync: sent' 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:
|
def test_hard_links(self, src: Path, out: Path) -> None:
|
||||||
assert run_backup(out, [str(src)]) == 0
|
assert run_backup(out, [str(src)]) == 0
|
||||||
(src / 'changed.txt').write_text('second')
|
(src / 'changed.txt').write_text('second')
|
||||||
@@ -231,6 +243,13 @@ class TestReadRecords:
|
|||||||
assert list(sb._read_records(stream)) == ['file', '', 'namé', '�']
|
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:
|
class TestProgressBar:
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def terminal(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
def terminal(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user