mirror of
https://github.com/kevinveenbirkenbach/docker-volume-backup.git
synced 2026-08-24 23:04:34 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 90d289d92f | |||
| 57fc7c96bc |
14
CHANGELOG.md
14
CHANGELOG.md
@@ -1,5 +1,19 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [3.4.3] - 2026-08-16
|
||||||
|
|
||||||
|
- Backup: *create_version_directory* carried *exist_ok=True*, so a run starting
|
||||||
|
in the same wall-clock second as its predecessor claimed that predecessor's
|
||||||
|
generation. Generation names carry seconds, and a host with little to copy
|
||||||
|
finishes inside one — rsync *--delete* then overwrote a finished generation,
|
||||||
|
and only afterwards did *create_stamp_file* refuse the already-stamped
|
||||||
|
directory and exit 2. The guard reported the damage instead of preventing it.
|
||||||
|
- Backup: the generation directory is claimed exclusively. Claiming it is the
|
||||||
|
first filesystem action of a run, so a collision aborts before the first
|
||||||
|
write and names the second it collided on.
|
||||||
|
- Tests: the idempotence test asserted the reuse and gave way to one that
|
||||||
|
requires the refusal.
|
||||||
|
|
||||||
## [3.4.2] - 2026-08-15
|
## [3.4.2] - 2026-08-15
|
||||||
|
|
||||||
- Backup: *has_image* matched the raw *.Config.Image*, so the registry host and
|
- Backup: *has_image* matched the raw *.Config.Image*, so the registry host and
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "backup-docker-to-local"
|
name = "backup-docker-to-local"
|
||||||
version = "3.4.2"
|
version = "3.4.3"
|
||||||
description = "Backup Docker volumes to local with rsync and optional DB dumps."
|
description = "Backup Docker volumes to local with rsync and optional DB dumps."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import pathlib
|
|||||||
|
|
||||||
from dirval import create_stamp_file
|
from dirval import create_stamp_file
|
||||||
|
|
||||||
from .shell import execute_shell_command
|
from .shell import BackupException, execute_shell_command
|
||||||
|
|
||||||
|
|
||||||
def get_machine_id() -> str:
|
def get_machine_id() -> str:
|
||||||
@@ -23,7 +23,14 @@ def stamp_directory(version_dir: str) -> None:
|
|||||||
|
|
||||||
def create_version_directory(versions_dir: str, backup_time: str) -> str:
|
def create_version_directory(versions_dir: str, backup_time: str) -> str:
|
||||||
version_dir = os.path.join(versions_dir, backup_time)
|
version_dir = os.path.join(versions_dir, backup_time)
|
||||||
pathlib.Path(version_dir).mkdir(parents=True, exist_ok=True)
|
try:
|
||||||
|
pathlib.Path(version_dir).mkdir(parents=True)
|
||||||
|
except FileExistsError:
|
||||||
|
raise BackupException(
|
||||||
|
f"generation {backup_time} already exists at {version_dir}; "
|
||||||
|
"another run claimed this second - refusing to write into it, "
|
||||||
|
"since rsync --delete would overwrite that generation"
|
||||||
|
) from None
|
||||||
return version_dir
|
return version_dir
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from pathlib import Path
|
|||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from baudolo.backup import layout as mod
|
from baudolo.backup import layout as mod
|
||||||
|
from baudolo.backup.shell import BackupException
|
||||||
|
|
||||||
|
|
||||||
class TestVersionDirectory(unittest.TestCase):
|
class TestVersionDirectory(unittest.TestCase):
|
||||||
@@ -17,11 +18,12 @@ class TestVersionDirectory(unittest.TestCase):
|
|||||||
self.assertTrue(Path(created).is_dir())
|
self.assertTrue(Path(created).is_dir())
|
||||||
self.assertEqual(Path(created).name, "20260731020304")
|
self.assertEqual(Path(created).name, "20260731020304")
|
||||||
|
|
||||||
def test_it_is_idempotent(self) -> None:
|
def test_it_refuses_a_generation_another_run_already_claimed(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
first = mod.create_version_directory(tmp, "20260731")
|
mod.create_version_directory(tmp, "20260731")
|
||||||
second = mod.create_version_directory(tmp, "20260731")
|
with self.assertRaises(BackupException) as caught:
|
||||||
self.assertEqual(first, second)
|
mod.create_version_directory(tmp, "20260731")
|
||||||
|
self.assertIn("20260731", str(caught.exception))
|
||||||
|
|
||||||
def test_it_creates_missing_parents(self) -> None:
|
def test_it_creates_missing_parents(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
|||||||
Reference in New Issue
Block a user