Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions rsync_time_machine.py
Original file line number Diff line number Diff line change
Expand Up @@ -694,14 +694,29 @@ def deal_with_no_space_left(
auto_expire: bool,
) -> bool:
"""Deal with no space left on device."""
with open(log_file) as f:
with open(log_file, encoding="utf-8", errors="replace") as f:
log_data = f.read()

no_space_left = re.search(
r"No space left on device \(28\)|Result too large \(34\)",
log_data,
)

if not no_space_left and re.search(
r"rsync: \[sender\] write error: Broken pipe \(32\)", log_data,
):
df_result = run_cmd(f"df -Pk '{dest_folder}'", dest_is_ssh(ssh))
if df_result.returncode == 0:
lines = df_result.stdout.splitlines()
if len(lines) > 1:
try:
available_blocks = int(lines[1].split()[3])
except (IndexError, ValueError):
pass
else:
if available_blocks <= 0:
no_space_left = True

if no_space_left:
if not auto_expire:
log_error(
Expand All @@ -727,7 +742,7 @@ def check_rsync_errors(
auto_delete_log: bool, # noqa: FBT001
) -> None:
"""Check rsync errors."""
with open(log_file) as f:
with open(log_file, encoding="utf-8", errors="replace") as f:
log_data = f.read()
if "rsync error:" in log_data:
log_error(
Expand Down
89 changes: 89 additions & 0 deletions tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
backup,
backup_marker_path,
check_dest_is_backup_folder,
deal_with_no_space_left,
expire_backups,
find,
find_backup_marker,
Expand Down Expand Up @@ -199,6 +200,94 @@ def test_find_backup_marker(tmp_path: Path) -> None:
assert find_backup_marker(str(tmp_path), None) == marker_path


def test_deal_with_no_space_left_handles_broken_pipe_when_dest_full(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Broken pipe with a full destination should trigger expiration."""
log_file = tmp_path / "2025-10-12-212400.log"
log_file.write_text(
"rsync: [sender] write error: Broken pipe (32)\n", encoding="utf-8",
)

backups = [
"/dest/2025-10-11-120000",
"/dest/2025-10-12-120000",
]

monkeypatch.setattr(rsync_time_machine, "find_backups", Mock(return_value=backups))

expired: list[str] = []
monkeypatch.setattr(
rsync_time_machine,
"expire_backup",
lambda path, ssh: expired.append(path),
)

def fake_run_cmd(
cmd: str, ssh: rsync_time_machine.SSH | None = None,
) -> rsync_time_machine.CmdResult:
if cmd.startswith("df -Pk"):
df_output = (
"Filesystem 1024-blocks Used Available Capacity Mounted on\n"
"/dev/sda1 100 100 0 100% /dest\n"
)
return rsync_time_machine.CmdResult(df_output, "", 0)
return rsync_time_machine.CmdResult("", "", 0)

monkeypatch.setattr(rsync_time_machine, "run_cmd", fake_run_cmd)

should_retry = deal_with_no_space_left(
str(log_file),
"/dest",
ssh=None,
auto_expire=True,
)

assert should_retry is True
assert expired == [sorted(backups)[-1]]


def test_deal_with_no_space_left_ignores_broken_pipe_when_space_available(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Broken pipe without the destination filling up should not expire backups."""
log_file = tmp_path / "2025-10-12-212400.log"
log_file.write_text(
"rsync: [sender] write error: Broken pipe (32)\n", encoding="utf-8",
)

monkeypatch.setattr(
rsync_time_machine,
"find_backups",
Mock(return_value=["/dest/2025-10-12-120000", "/dest/2025-10-11-120000"]),
)
monkeypatch.setattr(rsync_time_machine, "expire_backup", Mock())

def fake_run_cmd(
cmd: str, ssh: rsync_time_machine.SSH | None = None,
) -> rsync_time_machine.CmdResult:
if cmd.startswith("df -Pk"):
df_output = (
"Filesystem 1024-blocks Used Available Capacity Mounted on\n"
"/dev/sda1 100 58 42 58% /dest\n"
)
return rsync_time_machine.CmdResult(df_output, "", 0)
return rsync_time_machine.CmdResult("", "", 0)

monkeypatch.setattr(rsync_time_machine, "run_cmd", fake_run_cmd)

should_retry = deal_with_no_space_left(
str(log_file),
"/dest",
ssh=None,
auto_expire=True,
)

assert should_retry is False


def test_run_cmd() -> None:
"""Test the run_cmd function."""
result = run_cmd("echo 'Hello, World!'")
Expand Down
Loading