Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Improve "Auto-remove" ("Remove & Retention") GUI and its documentation #2000

Merged
merged 56 commits into from
Feb 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
cc92e06
x [skip ci]
buhtz Dec 25, 2024
042b676
rename auto-remove tab
buhtz Jan 2, 2025
69ab7e2
attention info label
buhtz Jan 2, 2025
ed4c665
HLineWidget
buhtz Jan 2, 2025
d2798f9
x
buhtz Jan 5, 2025
336bfc4
Merge branch 'dev' into fix/1976retention
buhtz Jan 9, 2025
4213e45
Merge branch 'dev' into fix/1976autoremove
buhtz Jan 9, 2025
6c046ed
Merge branch 'fix/1976autoremove' into fix/1976retention
buhtz Jan 9, 2025
1eb85d2
x [skip ci]
buhtz Jan 9, 2025
01f965a
remove debug code
buhtz Jan 9, 2025
3d1b337
groupbox [skip ci]
buhtz Jan 10, 2025
21b16c7
Merge branch 'dev' into fix/1976retention
buhtz Jan 12, 2025
78ea302
x
buhtz Jan 12, 2025
7986a0a
tooltips still missing
buhtz Jan 12, 2025
8604b03
renamed tab files
buhtz Jan 12, 2025
c1357f9
test RemoveOldSnapshotsDate. start here [skip ci]
buhtz Jan 13, 2025
c243d64
x
buhtz Jan 16, 2025
7b4c386
Merge branch 'dev' into fix/1976retention
buhtz Jan 19, 2025
8ffd942
doc [skip ci]
buhtz Jan 19, 2025
781ed78
doc [skip ci]
buhtz Jan 19, 2025
41f01f9
mkdocs [skip ci]
buhtz Jan 19, 2025
9dffd60
Merge branch 'dev' into fix/1976retention
buhtz Jan 21, 2025
9343bc8
x [skip ci]
buhtz Jan 22, 2025
bcf6231
Merge branch 'dev' into fix/1976retention
buhtz Jan 23, 2025
05a807e
doc
buhtz Jan 23, 2025
f491b69
x
buhtz Jan 23, 2025
5f10409
x
buhtz Jan 24, 2025
b8aa5d5
x [skip ci]
buhtz Jan 24, 2025
9d1c51b
x
buhtz Jan 25, 2025
720428b
x
buhtz Jan 26, 2025
d0dd2b1
minimize size
buhtz Jan 26, 2025
54c1297
mightBeRichText
buhtz Jan 26, 2025
b7043ff
x
buhtz Jan 26, 2025
b47f36e
x
buhtz Jan 26, 2025
ecb2c96
typos [skip ci]
buhtz Jan 26, 2025
e5f72b4
refactor open user manual
buhtz Jan 27, 2025
d6b3205
rule order lable link
buhtz Jan 27, 2025
34a8442
tooltips retention policy [skip ci]
buhtz Jan 27, 2025
a816334
tooltip typo [skip ci]
buhtz Jan 27, 2025
64cd3ec
x [skip ci]
buhtz Jan 28, 2025
2e50f0d
x
buhtz Jan 29, 2025
aa83b35
x
buhtz Jan 29, 2025
8af04b6
[skip ci]
buhtz Jan 29, 2025
37c032b
Merge branch 'dev' into fix/1976retention
buhtz Jan 30, 2025
825ce89
[skip ci]
buhtz Jan 30, 2025
2742b9a
x
buhtz Jan 31, 2025
ff5095f
retention policy finished. inode space missing
buhtz Jan 31, 2025
5aee8f1
removed deprecated smartremove unit tests
buhtz Jan 31, 2025
bb3793d
[skip ci]
buhtz Jan 31, 2025
7a0e500
rule
buhtz Jan 31, 2025
abfd15d
x
buhtz Jan 31, 2025
0a61323
x
buhtz Jan 31, 2025
1b8c01a
x
buhtz Jan 31, 2025
a875eb0
minor mods
buhtz Jan 31, 2025
efab395
Merge branch 'dev' into fix/1976retention
buhtz Feb 1, 2025
f219017
mkdocs site url [skip ci]
buhtz Feb 2, 2025
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
1 change: 1 addition & 0 deletions CHANGES
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
Back In Time

Version 1.6.0-dev (development of upcoming release)
* Doc: Remove & Retention (formally known as Auto-/Smart-Remove) with improved GUI and user manual section (#2000)
* Changed: Updated desktop entry files
* Changed: Move several values from config file into new introduce state file ($XDG_STATE_HOME/backintime.json)
* Fix: The width of the fourth column in files view is now saved
Expand Down
1 change: 1 addition & 0 deletions common/bitbase.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
USER_MANUAL_ONLINE_URL = 'https://backintime.readthedocs.io'
USER_MANUAL_LOCAL_PATH = Path('/') / 'usr' / 'share' / 'doc' / \
'backintime-common' / 'manual' / 'index.html'
USER_MANUAL_LOCAL_AVAILABLE = USER_MANUAL_LOCAL_PATH.exists()


class TimeUnit(Enum):
Expand Down
42 changes: 26 additions & 16 deletions common/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -943,26 +943,12 @@ def setKeepOnlyOneSnapshot(self, value, profile_id = None):
def removeOldSnapshotsEnabled(self, profile_id = None):
return self.profileBoolValue('snapshots.remove_old_snapshots.enabled', True, profile_id)

def removeOldSnapshotsDate(self, profile_id = None):
def removeOldSnapshotsDate(self, profile_id=None):
enabled, value, unit = self.removeOldSnapshots(profile_id)
if not enabled:
return datetime.date(1, 1, 1)

if unit == self.DAY:
date = datetime.date.today()
date = date - datetime.timedelta(days = value)
return date

if unit == self.WEEK:
date = datetime.date.today()
date = date - datetime.timedelta(days = date.weekday() + 7 * value)
return date

if unit == self.YEAR:
date = datetime.date.today()
return date.replace(day = 1, year = date.year - value)

return datetime.date(1, 1, 1)
return _remove_old_snapshots_date(value, unit)

def setRemoveOldSnapshots(self, enabled, value, unit, profile_id = None):
self.setProfileBoolValue('snapshots.remove_old_snapshots.enabled', enabled, profile_id)
Expand Down Expand Up @@ -1659,3 +1645,27 @@ def _cron_cmd(self, profile_id):
cmd = tools.which('nice') + ' -n19 ' + cmd

return cmd


def _remove_old_snapshots_date(value, unit):
"""Dev note (buhtz, 2025-01): The function exist to decople that code from
Config class and make it testable to investigate its behavior.

See issue #1943 for further reading.
"""
if unit == Config.DAY:
date = datetime.date.today()
date = date - datetime.timedelta(days=value)
return date

if unit == Config.WEEK:
date = datetime.date.today()
# Always beginning (Monday) of the week
date = date - datetime.timedelta(days=date.weekday() + 7 * value)
return date

if unit == Config.YEAR:
date = datetime.date.today()
return date.replace(day=1, year=date.year - value)

return datetime.date(1, 1, 1)
45 changes: 31 additions & 14 deletions common/snapshots.py
Original file line number Diff line number Diff line change
Expand Up @@ -1856,24 +1856,35 @@ def freeSpace(self, now):
Args:
now (datetime.datetime): Timestamp when takeSnapshot was started.
"""

# All existing snapshots, ordered from old to new.
# e.g. 2025-01-11 to 2025-01-19
snapshots = listSnapshots(self.config, reverse=False)

if not snapshots:
logger.debug('No snapshots. Skip freeSpace', self)
return

logger.debug(f'Backups from {snapshots[0]} to {snapshots[-1]}.', self)

last_snapshot = snapshots[-1]

# Remove old backups
# Remove snapshots older than N years/weeks/days
if self.config.removeOldSnapshotsEnabled():
self.setTakeSnapshotMessage(0, _('Removing old snapshots'))

oldBackupId = SID(self.config.removeOldSnapshotsDate(), self.config)
logger.debug("Remove snapshots older than: {}".format(oldBackupId.withoutTag), self)
# The oldest backup to keep. Others older than this are removed.
oldSID = SID(self.config.removeOldSnapshotsDate(), self.config)
oldBackupId = oldSID.withoutTag

logger.debug(f'Remove snapshots older than: {oldBackupId}', self)

while True:
# Keep min one backup
if len(snapshots) <= 1:
break
if snapshots[0] >= oldBackupId:

# ... younger or same as ...
if snapshots[0].withoutTag >= oldBackupId:
break

if self.config.dontRemoveNamedSnapshots():
Expand All @@ -1882,7 +1893,9 @@ def freeSpace(self, now):
continue

msg = 'Remove snapshot {} because it is older than {}'
logger.debug(msg.format(snapshots[0].withoutTag, oldBackupId.withoutTag), self)
logger.debug(msg.format(
snapshots[0].withoutTag, oldBackupId), self)

self.remove(snapshots[0])
del snapshots[0]

Expand Down Expand Up @@ -2427,26 +2440,29 @@ class SID:
"""
__cValidSID = re.compile(r'^\d{8}-\d{6}(?:-\d{3})?$')

INFO = 'info'
NAME = 'name'
FAILED = 'failed'
INFO = 'info'
NAME = 'name'
FAILED = 'failed'
FILEINFO = 'fileinfo.bz2'
LOG = 'takesnapshot.log.bz2'
LOG = 'takesnapshot.log.bz2'

def __init__(self, date, cfg):
self.config = cfg
self.profileID = cfg.currentProfile()
self.isRoot = False

if isinstance(date, datetime.datetime):
self.sid = '-'.join((date.strftime('%Y%m%d-%H%M%S'), self.config.tag(self.profileID)))
self.sid = '-'.join((date.strftime('%Y%m%d-%H%M%S'),
self.config.tag(self.profileID)))
# TODO: Don't use "date" as attribute name. Btw: It is not a date
# but a datetime.
self.date = date

elif isinstance(date, datetime.date):
self.sid = '-'.join((date.strftime('%Y%m%d-000000'), self.config.tag(self.profileID)))
self.date = datetime.datetime.combine(date, datetime.datetime.min.time())
self.sid = '-'.join((date.strftime('%Y%m%d-000000'),
self.config.tag(self.profileID)))
self.date = datetime.datetime.combine(
date, datetime.datetime.min.time())

elif isinstance(date, str):
if self.__cValidSID.match(date):
Expand Down Expand Up @@ -2756,7 +2772,8 @@ def lastChecked(self):
return time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(os.path.getatime(info)))
return self.displayID

#using @property.setter would be confusing here as there is no value to give
# using @property.setter would be confusing here as there is no value to
# give
def setLastChecked(self):
"""
Set info files atime to current time to indicate this snapshot was
Expand Down
62 changes: 61 additions & 1 deletion common/test/test_config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# SPDX-FileCopyrightText: © 2016 Taylor Raack
# SPDX-FileCopyrightText: © 2025 Christian Buhtz <[email protected]>
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
Expand All @@ -10,11 +11,70 @@
import os
import sys
import getpass
import unittest
import datetime
from unittest.mock import patch
from test import generic
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
import config


class TestSshCommand(generic.SSHTestCase):
class RemoveOldSnapshotsDate(unittest.TestCase):
def test_invalid_unit(self):
"""1st January Year 1 on errors"""
unit = 99999
value = 99

self.assertEqual(
config._remove_old_snapshots_date(value, unit),
datetime.date(1, 1, 1))

@patch('datetime.date', wraps=datetime.date)
def test_day(self, m):
"""Three days"""
m.today.return_value = datetime.date(2025, 1, 10)
sut = config._remove_old_snapshots_date(3, config.Config.DAY)
self.assertEqual(sut, datetime.date(2025, 1, 7))

@patch('datetime.date', wraps=datetime.date)
def test_week_always_monday(self, m):
"""Result is always a Monday"""

# 1-53 weeks back
for weeks in range(1, 54):
start = datetime.date(2026, 1, 1)

# Every day in the year
for count in range(366):
m.today.return_value = start - datetime.timedelta(days=count)

sut = config._remove_old_snapshots_date(
weeks, config.Config.WEEK)

# 0=Monday
self.assertEqual(sut.weekday(), 0, f'{sut=} {weeks=}')

@patch('datetime.date', wraps=datetime.date)
def test_week_ignore_current(self, m):
"""Current (incomplete) week is ignored."""
for day in range(25, 32): # Monday (25th) to Sunday (31th)
m.today.return_value = datetime.date(2025, 8, day)
sut = config._remove_old_snapshots_date(2, config.Config.WEEK)
self.assertEqual(
sut,
datetime.date(2025, 8, 11) # Monday
)

@patch('datetime.date', wraps=datetime.date)
def test_year_ignore_current_month(self, m):
"""Not years but 12 months are counted. But current month is
ignored."""
m.today.return_value = datetime.date(2025, 7, 30)
sut = config._remove_old_snapshots_date(2, config.Config.YEAR)
self.assertEqual(sut, datetime.date(2023, 7, 1))


class SshCommand(generic.SSHTestCase):
@classmethod
def setUpClass(cls):
cls._user = getpass.getuser()
Expand Down
Loading