diff --git a/src/btrfs2s3/backups.py b/src/btrfs2s3/backups.py index 6e42f82..238d794 100644 --- a/src/btrfs2s3/backups.py +++ b/src/btrfs2s3/backups.py @@ -23,6 +23,7 @@ _SNDP = "sndp" _PRNT = "prnt" _MDVN = "mdvn" +_SEQN = "seqn" _METADATA_VERSION = 1 @@ -93,6 +94,7 @@ def get_path_suffixes(self, *, tzinfo: tzinfo | str | None = None) -> Sequence[s f".{_SNDP}{send_parent_uuid}", f".{_PRNT}{parent_uuid}", f".{_MDVN}{_METADATA_VERSION}", + f".{_SEQN}0", ] @classmethod @@ -122,6 +124,7 @@ def from_path(cls, path: str) -> Self: # noqa: C901 ctransid: int | None = None ctime: Arrow | None = None version: int | None = None + sequence_number: int | None = None suffixes = pathlib.PurePath(path).suffixes for suffix in suffixes: @@ -144,6 +147,9 @@ def from_path(cls, path: str) -> Self: # noqa: C901 elif code == _MDVN: with suppress(ValueError): version = int(rest) + elif code == _SEQN: + with suppress(ValueError): + sequence_number = int(rest) if version is None: msg = "backup name metadata version missing (not a backup?)" @@ -151,6 +157,9 @@ def from_path(cls, path: str) -> Self: # noqa: C901 if version != _METADATA_VERSION: msg = "unsupported backup name metadata version" raise ValueError(msg) + if sequence_number != 0: + msg = "unsupported sequence number" + raise ValueError(msg) if ( uuid is None or parent_uuid is None diff --git a/tests/backups/path_test.py b/tests/backups/path_test.py index db59903..72e5c55 100644 --- a/tests/backups/path_test.py +++ b/tests/backups/path_test.py @@ -1,6 +1,7 @@ from __future__ import annotations import itertools +import random import re from typing import TYPE_CHECKING from uuid import UUID @@ -11,6 +12,9 @@ if TYPE_CHECKING: from typing import Sequence + from typing import TypeVar + + _T = TypeVar("_T") def test_get_path_suffixes_with_real_timezone() -> None: @@ -29,6 +33,7 @@ def test_get_path_suffixes_with_real_timezone() -> None: ".sndp3ae01eae-d50d-4187-b67f-cef0ef973e1f", ".prnt9d9d3bcb-4b62-46a3-b6e2-678eeb24f54e", ".mdvn1", + ".seqn0", ] assert got == expected @@ -52,6 +57,7 @@ def test_get_path_suffixes_default_to_utc() -> None: ".sndp3ae01eae-d50d-4187-b67f-cef0ef973e1f", ".prnt9d9d3bcb-4b62-46a3-b6e2-678eeb24f54e", ".mdvn1", + ".seqn0", ] assert got == expected @@ -75,6 +81,7 @@ def test_get_path_suffixes_with_full_backup() -> None: ".sndp00000000-0000-0000-0000-000000000000", ".prnt9d9d3bcb-4b62-46a3-b6e2-678eeb24f54e", ".mdvn1", + ".seqn0", ] assert got == expected @@ -82,18 +89,33 @@ def test_get_path_suffixes_with_full_backup() -> None: assert round_trip == info +def _fixed_choices(population: Sequence[_T], seed: int, k: int) -> list[_T]: + state = random.getstate() + random.seed(seed) + result = random.choices(population, k=k) + random.setstate(state) + return result + + @pytest.mark.parametrize( "suffixes", - itertools.permutations( - [ - ".ctim2006-01-01T00:00:00-08:00", - ".ctid12345", - ".uuid3fd11d8e-8110-4cd0-b85c-bae3dda86a3d", - ".sndp3ae01eae-d50d-4187-b67f-cef0ef973e1f", - ".prnt9d9d3bcb-4b62-46a3-b6e2-678eeb24f54e", - ".mdvn1", - ".gz", - ] + _fixed_choices( + list( + itertools.permutations( + [ + ".ctim2006-01-01T00:00:00-08:00", + ".ctid12345", + ".uuid3fd11d8e-8110-4cd0-b85c-bae3dda86a3d", + ".sndp3ae01eae-d50d-4187-b67f-cef0ef973e1f", + ".prnt9d9d3bcb-4b62-46a3-b6e2-678eeb24f54e", + ".mdvn1", + ".seqn0", + ".gz", + ] + ) + ), + 0, + 1000, ), ) def test_from_path_with_suffixes_in_any_order(suffixes: Sequence[str]) -> None: @@ -117,30 +139,35 @@ def test_from_path_with_suffixes_in_any_order(suffixes: Sequence[str]) -> None: ".uuid3fd11d8e-8110-4cd0-b85c-bae3dda86a3d" ".sndp3ae01eae-d50d-4187-b67f-cef0ef973e1f" ".prnt9d9d3bcb-4b62-46a3-b6e2-678eeb24f54e" - ".mdvn1", + ".mdvn1" + ".seqn0", ".ctim2006-01-01T00:00:00-08:00" ".ctid12345" ".uuid3fd11d8e-811O-4cd0-b85c-bae3dda86a3d" ".sndp3ae01eae-d50d-4187-b67f-cef0ef973e1f" ".prnt9d9d3bcb-4b62-46a3-b6e2-678eeb24f54e" - ".mdvn1", + ".mdvn1" + ".seqn0", ".ctim2006-01-01T00:00:00-08:00" ".ctidl2345.u3fd11d8e-8110-4cd0-b85c-bae3dda86a3d" ".sndp3ae01eae-d50d-4187-b67f-cef0ef973e1f" ".prnt9d9d3bcb-4b62-46a3-b6e2-678eeb24f54e" - ".mdvn1", + ".mdvn1" + ".seqn0", ".ctim2006-01-01T00:00:00-08:00" ".ctid12345" ".uuid3fd11d8e-8110-4cd0-b85c-bae3dda86a3d" ".sndp3ae01eae-d50d-4187-b67f-cef0ef973e1f" ".prnt9d9d3bcb-4b62-46a3-b6e2-678eeb24f54e" - ".mdvn1", + ".mdvn1" + ".seqn0", ".ctim2006-01-01T00:00:00-08:00" ".ctid12345" ".uuid3fd11d8e-8110-4cd0-b85c-bae3dda86a3d" ".sndp3ae01eae-d50d-4187-b67f-cef0ef973e1f" ".prnt9d9d3gcb-4b62-46a3-b6e2-678eeb24f54e" - ".mdvn1", + ".mdvn1" + ".seqn0", ], ) def test_bad_paths(bad_path: str) -> None: @@ -159,13 +186,15 @@ def test_bad_paths(bad_path: str) -> None: ".ctid12345" ".uuid3fd11d8e-8110-4cd0-b85c-bae3dda86a3d" ".sndp3ae01eae-d50d-4187-b67f-cef0ef973e1f" - ".prnt9d9d3bcb-4b62-46a3-b6e2-678eeb24f54e", + ".prnt9d9d3bcb-4b62-46a3-b6e2-678eeb24f54e" + ".seqn0", ".ctim2006-01-01T00:00:00-08:00" ".ctid12345" ".uuid3fd11d8e-8110-4cd0-b85c-bae3dda86a3d" ".sndp3ae01eae-d50d-4187-b67f-cef0ef973e1f" ".prnt9d9d3bcb-4b62-46a3-b6e2-678eeb24f54e" - ".mdvnI", + ".mdvnI" + ".seqn0", ], ) def test_no_version(bad_path: str) -> None: @@ -185,4 +214,18 @@ def test_bad_version() -> None: ".sndp3ae01eae-d50d-4187-b67f-cef0ef973e1f" ".prnt9d9d3bcb-4b62-46a3-b6e2-678eeb24f54e" ".mdvn1000" + ".seqn0" + ) + + +def test_bad_sequence_number() -> None: + with pytest.raises(ValueError, match="unsupported sequence number"): + BackupInfo.from_path( + ".ctim2006-01-01T00:00:00-08:00" + ".ctid12345" + ".uuid3fd11d8e-8110-4cd0-b85c-bae3dda86a3d" + ".sndp3ae01eae-d50d-4187-b67f-cef0ef973e1f" + ".prnt9d9d3bcb-4b62-46a3-b6e2-678eeb24f54e" + ".mdvn1" + ".seqn1" )