Skip to content

Commit e5471c8

Browse files
committed
Support for filename patterns in configuration files (#10)
For details refer to the following feature request: #10
1 parent f1aec8d commit e5471c8

File tree

3 files changed

+66
-12
lines changed

3 files changed

+66
-12
lines changed

requirements.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
coloredlogs >= 5.0
2-
executor >= 16.0.1
1+
coloredlogs >= 5.1
2+
executor >= 19.2
33
humanfriendly >= 3.4
44
naturalsort >= 1.4
55
property-manager >= 2.3
@@ -8,4 +8,4 @@ python-dateutil >= 2.2
88
simpleeval >= 0.8.7, < 0.9.2
99
six >= 1.9.0
1010
update-dotdee >= 5.0
11-
verboselogs >= 1.4
11+
verboselogs >= 1.5

rotate_backups/__init__.py

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -146,12 +146,14 @@ def coerce_retention_period(value):
146146
return value
147147

148148

149-
def load_config_file(configuration_file=None):
149+
def load_config_file(configuration_file=None, expand=True):
150150
"""
151151
Load a configuration file with backup directories and rotation schemes.
152152
153153
:param configuration_file: Override the pathname of the configuration file
154154
to load (a string or :data:`None`).
155+
:param expand: :data:`True` to expand filename patterns to their matches,
156+
:data:`False` otherwise.
155157
:returns: A generator of tuples with four values each:
156158
157159
1. An execution context created using :mod:`executor.contexts`.
@@ -175,6 +177,7 @@ def load_config_file(configuration_file=None):
175177
above, so that sections in user-specific configuration files override
176178
sections by the same name in system-wide configuration files.
177179
"""
180+
expand_notice_given = False
178181
if configuration_file:
179182
loader = ConfigLoader(available_files=[configuration_file], strict=True)
180183
else:
@@ -195,7 +198,21 @@ def load_config_file(configuration_file=None):
195198
io_scheduling_class=items.get('ionice'),
196199
strict=coerce_boolean(items.get('strict', 'yes')),
197200
prefer_recent=coerce_boolean(items.get('prefer-recent', 'no')))
198-
yield location, rotation_scheme, options
201+
# Expand filename patterns?
202+
if expand and location.have_wildcards:
203+
logger.verbose("Expanding filename pattern %s on %s ..", location.directory, location.context)
204+
if location.is_remote and not expand_notice_given:
205+
logger.notice("Expanding remote filename patterns (may be slow) ..")
206+
expand_notice_given = True
207+
for match in sorted(location.context.glob(location.directory)):
208+
if location.context.is_directory(match):
209+
logger.verbose("Matched directory: %s", match)
210+
expanded = Location(context=location.context, directory=match)
211+
yield expanded, rotation_scheme, options
212+
else:
213+
logger.verbose("Ignoring match (not a directory): %s", match)
214+
else:
215+
yield location, rotation_scheme, options
199216

200217

201218
def rotate_backups(directory, rotation_scheme, **options):
@@ -472,15 +489,27 @@ def load_config_file(self, location):
472489
:returns: The configured or given :class:`Location` object.
473490
"""
474491
location = coerce_location(location)
475-
for configured_location, rotation_scheme, options in load_config_file(self.config_file):
476-
if location == configured_location:
492+
for configured_location, rotation_scheme, options in load_config_file(self.config_file, expand=False):
493+
if configured_location.match(location):
477494
logger.verbose("Loading configuration for %s ..", location)
478495
if rotation_scheme:
479496
self.rotation_scheme = rotation_scheme
480497
for name, value in options.items():
481498
if value:
482499
setattr(self, name, value)
483-
return configured_location
500+
# Create a new Location object based on the directory of the
501+
# given location and the execution context of the configured
502+
# location, because:
503+
#
504+
# 1. The directory of the configured location may be a filename
505+
# pattern whereas we are interested in the expanded name.
506+
#
507+
# 2. The execution context of the given location may lack some
508+
# details of the configured location.
509+
return Location(
510+
context=configured_location.context,
511+
directory=location.directory,
512+
)
484513
logger.verbose("No configuration found for %s.", location)
485514
return location
486515

@@ -622,6 +651,11 @@ def have_ionice(self):
622651
""":data:`True` when ionice_ is available, :data:`False` otherwise."""
623652
return self.context.have_ionice
624653

654+
@lazy_property
655+
def have_wildcards(self):
656+
""":data:`True` if :attr:`directory` is a filename pattern, :data:`False` otherwise."""
657+
return '*' in self.directory
658+
625659
@lazy_property
626660
def mount_point(self):
627661
"""
@@ -703,6 +737,27 @@ def ensure_writable(self):
703737
to permissions. Consider using the --use-sudo option.
704738
""", location=self))
705739

740+
def match(self, location):
741+
"""
742+
Check if the given location "matches".
743+
744+
:param location: The :class:`Location` object to try to match.
745+
:returns: :data:`True` if the two locations are on the same system and
746+
the :attr:`directory` can be matched as a filename pattern or
747+
a literal match on the normalized pathname.
748+
"""
749+
if self.ssh_alias != location.ssh_alias:
750+
# Never match locations on other systems.
751+
return False
752+
elif self.have_wildcards:
753+
# Match filename patterns using fnmatch().
754+
return fnmatch.fnmatch(location.directory, self.directory)
755+
else:
756+
# Compare normalized directory pathnames.
757+
self = os.path.normpath(self.directory)
758+
other = os.path.normpath(location.directory)
759+
return self == other
760+
706761
def __str__(self):
707762
"""Render a simple human readable representation of a location."""
708763
return '%s:%s' % (self.ssh_alias, self.directory) if self.ssh_alias else self.directory

rotate_backups/cli.py

Lines changed: 3 additions & 4 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: March 25, 2018
4+
# Last Change: April 27, 2018
55
# URL: https://github.com/xolox/python-rotate-backups
66

77
"""
@@ -260,9 +260,8 @@ def main():
260260
# Rotation of all configured locations.
261261
location_source = 'configuration file'
262262
selected_locations.extend(
263-
location
264-
for location, rotation_scheme, options
265-
in load_config_file(kw.get('config_file'))
263+
location for location, rotation_scheme, options
264+
in load_config_file(configuration_file=kw.get('config_file'), expand=True)
266265
)
267266
# Inform the user which location(s) will be rotated.
268267
if selected_locations:

0 commit comments

Comments
 (0)