diff --git a/src/borg/archiver/prune_cmd.py b/src/borg/archiver/prune_cmd.py index 2ba9d3b67..7d0d66571 100644 --- a/src/borg/archiver/prune_cmd.py +++ b/src/borg/archiver/prune_cmd.py @@ -53,10 +53,8 @@ def prune_split(archives, rule, n, kept_because=None): a = None for a in sorted(archives, key=attrgetter("ts"), reverse=True): - # we compute the pruning in UTC time zone - # note: we used to compute the pruning in local timezone (tz=None), - # but this is causing test failures in some time zones (like e.g. UTC+12). - period = a.ts.astimezone(tz=timezone.utc).strftime(pattern) + # we compute the pruning in local time zone + period = a.ts.astimezone().strftime(pattern) if period != last: last = period if a.id not in kept_because: @@ -216,12 +214,12 @@ def build_parser_prune(self, subparsers, common_parser, mid_common_parser): up to 7 most recent days with backups (days without backups do not count). The rules are applied from secondly to yearly, and backups selected by previous rules do not count towards those of later rules. The time that each backup - starts is used for pruning purposes. Dates and times are interpreted in - the local timezone, and weeks go from Monday to Sunday. Specifying a - negative number of archives to keep means that there is no limit. As of borg - 1.2.0, borg will retain the oldest archive if any of the secondly, minutely, - hourly, daily, weekly, monthly, or yearly rules was not otherwise able to meet - its retention target. This enables the first chronological archive to continue + starts is used for pruning purposes. Dates and times are interpreted in the local + timezone of the system where borg prune runs, and weeks go from Monday to Sunday. + Specifying a negative number of archives to keep means that there is no limit. + As of borg 1.2.0, borg will retain the oldest archive if any of the secondly, + minutely, hourly, daily, weekly, monthly, or yearly rules was not otherwise able to + meet its retention target. This enables the first chronological archive to continue aging until it is replaced by a newer archive that meets the retention criteria. The ``--keep-last N`` option is doing the same as ``--keep-secondly N`` (and it will diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py index 7b0830d9a..5cf1900d9 100644 --- a/src/borg/testsuite/helpers.py +++ b/src/borg/testsuite/helpers.py @@ -405,6 +405,17 @@ def __repr__(self): return f"{self.id}: {self.ts.isoformat()}" +# This is the local timezone of the system running the tests. +# We need this e.g. to construct archive timestamps for the prune tests, +# because borg prune operates in the local timezone (it first converts the +# archive timestamp to the local timezone). So, if we want the y/m/d/h/m/s +# values which prune uses to be exactly the ones we give [and NOT shift them +# by tzoffset], we need to give the timestamps in the same local timezone. +# Please note that the timestamps in a real borg archive or manifest are +# stored in UTC timezone. +local_tz = datetime.now(tz=timezone.utc).astimezone(tz=None).tzinfo + + @pytest.mark.parametrize( "rule,num_to_keep,expected_ids", [ @@ -424,23 +435,23 @@ def subset(lst, ids): archives = [ # years apart - MockArchive(datetime(2015, 1, 1, 10, 0, 0, tzinfo=timezone.utc), 1), - MockArchive(datetime(2016, 1, 1, 10, 0, 0, tzinfo=timezone.utc), 2), - MockArchive(datetime(2017, 1, 1, 10, 0, 0, tzinfo=timezone.utc), 3), + MockArchive(datetime(2015, 1, 1, 10, 0, 0, tzinfo=local_tz), 1), + MockArchive(datetime(2016, 1, 1, 10, 0, 0, tzinfo=local_tz), 2), + MockArchive(datetime(2017, 1, 1, 10, 0, 0, tzinfo=local_tz), 3), # months apart - MockArchive(datetime(2017, 2, 1, 10, 0, 0, tzinfo=timezone.utc), 4), - MockArchive(datetime(2017, 3, 1, 10, 0, 0, tzinfo=timezone.utc), 5), + MockArchive(datetime(2017, 2, 1, 10, 0, 0, tzinfo=local_tz), 4), + MockArchive(datetime(2017, 3, 1, 10, 0, 0, tzinfo=local_tz), 5), # days apart - MockArchive(datetime(2017, 3, 2, 10, 0, 0, tzinfo=timezone.utc), 6), - MockArchive(datetime(2017, 3, 3, 10, 0, 0, tzinfo=timezone.utc), 7), - MockArchive(datetime(2017, 3, 4, 10, 0, 0, tzinfo=timezone.utc), 8), + MockArchive(datetime(2017, 3, 2, 10, 0, 0, tzinfo=local_tz), 6), + MockArchive(datetime(2017, 3, 3, 10, 0, 0, tzinfo=local_tz), 7), + MockArchive(datetime(2017, 3, 4, 10, 0, 0, tzinfo=local_tz), 8), # minutes apart - MockArchive(datetime(2017, 10, 1, 9, 45, 0, tzinfo=timezone.utc), 9), - MockArchive(datetime(2017, 10, 1, 9, 55, 0, tzinfo=timezone.utc), 10), + MockArchive(datetime(2017, 10, 1, 9, 45, 0, tzinfo=local_tz), 9), + MockArchive(datetime(2017, 10, 1, 9, 55, 0, tzinfo=local_tz), 10), # seconds apart - MockArchive(datetime(2017, 10, 1, 10, 0, 1, tzinfo=timezone.utc), 11), - MockArchive(datetime(2017, 10, 1, 10, 0, 3, tzinfo=timezone.utc), 12), - MockArchive(datetime(2017, 10, 1, 10, 0, 5, tzinfo=timezone.utc), 13), + MockArchive(datetime(2017, 10, 1, 10, 0, 1, tzinfo=local_tz), 11), + MockArchive(datetime(2017, 10, 1, 10, 0, 3, tzinfo=local_tz), 12), + MockArchive(datetime(2017, 10, 1, 10, 0, 5, tzinfo=local_tz), 13), ] kept_because = {} keep = prune_split(archives, rule, num_to_keep, kept_because) @@ -456,12 +467,12 @@ def subset(lst, ids): archives = [ # oldest backup, but not last in its year - MockArchive(datetime(2018, 1, 1, 10, 0, 0, tzinfo=timezone.utc), 1), + MockArchive(datetime(2018, 1, 1, 10, 0, 0, tzinfo=local_tz), 1), # an interim backup - MockArchive(datetime(2018, 12, 30, 10, 0, 0, tzinfo=timezone.utc), 2), + MockArchive(datetime(2018, 12, 30, 10, 0, 0, tzinfo=local_tz), 2), # year end backups - MockArchive(datetime(2018, 12, 31, 10, 0, 0, tzinfo=timezone.utc), 3), - MockArchive(datetime(2019, 12, 31, 10, 0, 0, tzinfo=timezone.utc), 4), + MockArchive(datetime(2018, 12, 31, 10, 0, 0, tzinfo=local_tz), 3), + MockArchive(datetime(2019, 12, 31, 10, 0, 0, tzinfo=local_tz), 4), ] # Keep oldest when retention target can't otherwise be met