Skip to content

Commit c4ae49f

Browse files
committed
Generalize --use-rmdir to --removal-command, add tests (#11)
1 parent bae7a05 commit c4ae49f

File tree

4 files changed

+84
-46
lines changed

4 files changed

+84
-46
lines changed

README.rst

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,14 @@ intended you have no right to complain ;-).
211211
remote system over SSH)."
212212
"``-n``, ``--dry-run``","Don't make any changes, just print what would be done. This makes it easy
213213
to evaluate the impact of a rotation scheme without losing any backups."
214-
"``-D``, ``--use-rmdir``","Use ""rmdir"" to remove the backups (useful with CephFS snapshots for example)."
214+
"``-C``, ``--removal-command=CMD``","Change the command used to remove backups. The value of ``CMD`` defaults to
215+
``rm ``-f``R``. This choice was made because it works regardless of whether
216+
""backups to be rotated"" are files or directories or a mixture of both.
217+
218+
As an example of why you might want to change this, CephFS snapshots are
219+
represented as regular directory trees that can be deleted at once with a
220+
single 'rmdir' command (even though according to POSIX semantics this
221+
command should refuse to remove nonempty directories, but I digress)."
215222
"``-v``, ``--verbose``",Increase logging verbosity (can be repeated).
216223
"``-q``, ``--quiet``",Decrease logging verbosity (can be repeated).
217224
"``-h``, ``--help``",Show this message and exit.
@@ -360,8 +367,11 @@ Supported configuration options
360367
- If an include or exclude list is defined in the configuration file it
361368
overrides the include or exclude list given on the command line.
362369

363-
- The ``prefer-recent``, ``strict``, ``rmdir`` and ``use-sudo`` options expect a
364-
boolean value (``yes``, ``no``, ``true``, ``false``, ``1`` or ``0``).
370+
- The ``prefer-recent``, ``strict`` and ``use-sudo`` options expect a boolean
371+
value (``yes``, ``no``, ``true``, ``false``, ``1`` or ``0``).
372+
373+
- The ``removal-command`` option can be used to customize the command that is
374+
used to remove backups.
365375

366376
- The ``ionice`` option expects one of the I/O scheduling class names ``idle``,
367377
``best-effort`` or ``realtime``.

rotate_backups/__init__.py

Lines changed: 41 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# rotate-backups: Simple command line interface for backup rotation.
22
#
33
# Author: Peter Odding <peter@peterodding.com>
4-
# Last Change: April 27, 2018
4+
# Last Change: August 2, 2018
55
# URL: https://github.com/xolox/python-rotate-backups
66

77
"""
@@ -19,6 +19,7 @@
1919
import numbers
2020
import os
2121
import re
22+
import shlex
2223

2324
# External dependencies.
2425
from dateutil.relativedelta import relativedelta
@@ -197,8 +198,11 @@ def load_config_file(configuration_file=None, expand=True):
197198
exclude_list=split(items.get('exclude-list', '')),
198199
io_scheduling_class=items.get('ionice'),
199200
strict=coerce_boolean(items.get('strict', 'yes')),
200-
prefer_recent=coerce_boolean(items.get('prefer-recent', 'no')),
201-
rmdir=items.get('use-rmdir'))
201+
prefer_recent=coerce_boolean(items.get('prefer-recent', 'no')))
202+
# Don't override the value of the 'removal_command' property unless the
203+
# 'removal-command' configuration file option has a value set.
204+
if items.get('removal-command'):
205+
options['removal_command'] = shlex.split(items['removal-command'])
202206
# Expand filename patterns?
203207
if expand and location.have_wildcards:
204208
logger.verbose("Expanding filename pattern %s on %s ..", location.directory, location.context)
@@ -240,11 +244,12 @@ def __init__(self, rotation_scheme, **options):
240244
Initialize a :class:`RotateBackups` object.
241245
242246
:param rotation_scheme: Used to set :attr:`rotation_scheme`.
243-
:param options: Any keyword arguments are used to set the values of the
244-
properties :attr:`config_file`, :attr:`dry_run`,
245-
:attr:`rmdir`, :attr:`exclude_list`,
246-
:attr:`include_list`, :attr:`io_scheduling_class` and
247-
:attr:`strict`.
247+
:param options: Any keyword arguments are used to set the values of
248+
instance properties that support assignment
249+
(:attr:`config_file`, :attr:`dry_run`,
250+
:attr:`exclude_list`, :attr:`include_list`,
251+
:attr:`io_scheduling_class`, :attr:`removal_command`
252+
and :attr:`strict`).
248253
"""
249254
options.update(rotation_scheme=rotation_scheme)
250255
super(RotateBackups, self).__init__(**options)
@@ -271,18 +276,6 @@ def dry_run(self):
271276
"""
272277
return False
273278

274-
@mutable_property
275-
def rmdir(self):
276-
"""
277-
:data:`True` to use `rmdir` to remove the snapshots, :data:`False` to use `rm -r` (defaults to :data:`False`).
278-
279-
Normally the backups are removed one file at a file using the command `rm -r`.
280-
Some file-systems are capable of removing a whole directory with the command `rmdir`,
281-
even when the directory is not empty. For example, this is how CephFS snapshots are
282-
removed.
283-
"""
284-
return False
285-
286279
@cached_property(writable=True)
287280
def exclude_list(self):
288281
"""
@@ -339,6 +332,24 @@ def prefer_recent(self):
339332
"""
340333
return False
341334

335+
@mutable_property
336+
def removal_command(self):
337+
"""
338+
The command used to remove backups (a list of strings).
339+
340+
By default the command ``rm -fR`` is used. This choice was made because
341+
it works regardless of whether the user's "backups to be rotated" are
342+
files or directories or a mixture of both.
343+
344+
.. versionadded: 5.3
345+
This option was added as a generalization of the idea suggested in
346+
`pull request 11`_, which made it clear to me that being able to
347+
customize the removal command has its uses.
348+
349+
.. _pull request 11: https://github.com/xolox/python-rotate-backups/pull/11
350+
"""
351+
return ['rm', '-fR']
352+
342353
@required_property
343354
def rotation_scheme(self):
344355
"""
@@ -481,18 +492,16 @@ class together to implement backup rotation with an easy to use Python
481492
else:
482493
logger.info("Deleting %s ..", friendly_name)
483494
if not self.dry_run:
484-
if not self.rmdir:
485-
command = location.context.prepare(
486-
'rm', '-Rf', backup.pathname,
487-
group_by=(location.ssh_alias, location.mount_point),
488-
ionice=self.io_scheduling_class,
489-
)
490-
else:
491-
command = location.context.prepare(
492-
'rmdir', backup.pathname,
493-
group_by=(location.ssh_alias, location.mount_point),
494-
ionice=self.io_scheduling_class,
495-
)
495+
# Copy the list with the (possibly user defined) removal command.
496+
removal_command = list(self.removal_command)
497+
# Add the pathname of the backup as the final argument.
498+
removal_command.append(backup.pathname)
499+
# Construct the command object.
500+
command = location.context.prepare(
501+
command=removal_command,
502+
group_by=(location.ssh_alias, location.mount_point),
503+
ionice=self.io_scheduling_class,
504+
)
496505
rotation_commands.append(command)
497506
if not prepare:
498507
timer = Timer()

rotate_backups/cli.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# rotate-backups: Simple command line interface for backup rotation.
22
#
33
# Author: Peter Odding <peter@peterodding.com>
4-
# Last Change: April 27, 2018
4+
# Last Change: August 2, 2018
55
# URL: https://github.com/xolox/python-rotate-backups
66

77
"""
@@ -154,11 +154,16 @@
154154
Don't make any changes, just print what would be done. This makes it easy
155155
to evaluate the impact of a rotation scheme without losing any backups.
156156
157-
-D, --use-rmdir
157+
-C, --removal-command=CMD
158158
159-
Remove backups by calling `rmdir' to directly remove the whole directory
160-
instead of deleting each of the files in it. This works only with special
161-
directories such as CephFS snapshot.
159+
Change the command used to remove backups. The value of CMD defaults to
160+
``rm -fR``. This choice was made because it works regardless of whether
161+
"backups to be rotated" are files or directories or a mixture of both.
162+
163+
As an example of why you might want to change this, CephFS snapshots are
164+
represented as regular directory trees that can be deleted at once with a
165+
single 'rmdir' command (even though according to POSIX semantics this
166+
command should refuse to remove nonempty directories, but I digress).
162167
163168
-v, --verbose
164169
@@ -175,6 +180,7 @@
175180

176181
# Standard library modules.
177182
import getopt
183+
import shlex
178184
import sys
179185

180186
# External dependencies.
@@ -208,11 +214,11 @@ def main():
208214
selected_locations = []
209215
# Parse the command line arguments.
210216
try:
211-
options, arguments = getopt.getopt(sys.argv[1:], 'M:H:d:w:m:y:I:x:jpri:c:r:uDnvqh', [
217+
options, arguments = getopt.getopt(sys.argv[1:], 'M:H:d:w:m:y:I:x:jpri:c:r:uC:nvqh', [
212218
'minutely=', 'hourly=', 'daily=', 'weekly=', 'monthly=', 'yearly=',
213219
'include=', 'exclude=', 'parallel', 'prefer-recent', 'relaxed',
214-
'ionice=', 'config=', 'use-sudo', 'dry-run', 'use-rmdir', 'verbose', 'quiet',
215-
'help',
220+
'ionice=', 'config=', 'use-sudo', 'dry-run', 'removal-command=',
221+
'verbose', 'quiet', 'help',
216222
])
217223
for option, value in options:
218224
if option in ('-M', '--minutely'):
@@ -247,8 +253,10 @@ def main():
247253
elif option in ('-n', '--dry-run'):
248254
logger.info("Performing a dry run (because of %s option) ..", option)
249255
kw['dry_run'] = True
250-
elif option in ('-D', '--use-rmdir'):
251-
kw['rmdir'] = True
256+
elif option in ('-C', '--removal-command'):
257+
removal_command = shlex.split(value)
258+
logger.info("Using custom removal command: %s", removal_command)
259+
kw['removal_command'] = removal_command
252260
elif option in ('-v', '--verbose'):
253261
coloredlogs.increase_verbosity()
254262
elif option in ('-q', '--quiet'):

rotate_backups/tests.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
# Test suite for the `rotate-backups' Python package.
22
#
33
# Author: Peter Odding <peter@peterodding.com>
4-
# Last Change: April 27, 2018
4+
# Last Change: August 3, 2018
55
# URL: https://github.com/xolox/python-rotate-backups
66

77
"""Test suite for the `rotate-backups` package."""
88

99
# Standard library modules.
10+
import datetime
1011
import logging
1112
import os
1213

@@ -369,6 +370,16 @@ def test_minutely_rotation(self):
369370
assert os.path.exists(os.path.join(root, 'backup-2016-01-10_21-30-00'))
370371
assert os.path.exists(os.path.join(root, 'backup-2016-01-10_21-45-00'))
371372

373+
def test_removal_command(self):
374+
"""Test that the removal command can be customized."""
375+
with TemporaryDirectory(prefix='rotate-backups-', suffix='-test-suite') as root:
376+
today = datetime.datetime.now()
377+
for date in today, (today - datetime.timedelta(hours=24)):
378+
os.mkdir(os.path.join(root, date.strftime('%Y-%m-%d')))
379+
program = RotateBackups(removal_command=['rmdir'], rotation_scheme=dict(monthly='always'))
380+
commands = program.rotate_backups(root, prepare=True)
381+
assert any(cmd.command_line[0] == 'rmdir' for cmd in commands)
382+
372383
def test_filename_patterns(self):
373384
"""Test support for filename patterns in configuration files."""
374385
with TemporaryDirectory(prefix='rotate-backups-', suffix='-test-suite') as root:

0 commit comments

Comments
 (0)