@@ -29,16 +29,45 @@ def pstr(path: bytes) -> str:
def format_size(n: int) -> str:
return humanfriendly.format_size(n, keep_width=True, binary=True)
# Type corresponding to patterns that are generated by
# wcmatch.translate: two lists of compiled REs (a,b). A path matches
# if it matches at least one regex in "a" and none in "b".
MatchPatterns = typing.Tuple[typing.List[re.Pattern], typing.List[re.Pattern]]
class Config:
roots: typing.List[bytes]
max_file_size: typing.Optional[int]
one_file_system: bool
exclude_caches: bool
exclude: typing.List[bytes]
force_include: typing.List[bytes]
exclude: MatchPatterns
unexclude: MatchPatterns
notify_email: typing.Optional[str]
def __init__(self, configfile: str):
# Helper to process lists of patterns into regexes
def process_match_list(config_name):
raw = config.get(config_name, '').encode().split(b'\n')
pats = []
# Prepend '**/' to any relative patterns
for x in raw:
if not len(x):
continue
if x.startswith(b'/'):
pats.append(x)
else:
pats.append(b'**/' + x)
# Compile patterns.
(a, b) = wcmatch.glob.translate(
pats, flags=(wcmatch.glob.GLOBSTAR |
wcmatch.glob.DOTGLOB |
wcmatch.glob.NODOTDIR |
wcmatch.glob.EXTGLOB |
wcmatch.glob.BRACE))
return ([ re.compile(x) for x in a ],
[ re.compile(x) for x in b ])
# Read config
with open(configfile, 'r') as f:
config = yaml.safe_load(f)
@@ -59,50 +88,17 @@ class Config:
self.roots.append(x)
self.roots.sort(key=len)
def process_match_list(config_name):
raw = config.get(config_name, '').encode().split(b'\n')
pats = []
# Prepend '**/' to any relative patterns
for x in raw:
if not len(x):
continue
if x.startswith(b'/'):
pats.append(x)
else:
pats.append(b'**/' + x)
return pats
self.exclude = process_match_list('exclude')
self.force_include = process_match_list('force-in clude')
self.unexclude = process_match_list('unexclude')
self.notify_email = config.get('notify-email', None)
# Compile patterns
flags = (wcmatch.glob.GLOBSTAR |
wcmatch.glob.DOTGLOB |
wcmatch.glob.NODOTDIR |
wcmatch.glob.EXTGLOB |
wcmatch.glob.BRACE)
# Path matches if it matches at least one regex in "a" and no
# regex in "b"
(a, b) = wcmatch.glob.translate(self.exclude, flags=flags)
self.exclude_re = ([ re.compile(x) for x in a ],
[ re.compile(x) for x in b ])
(a, b) = wcmatch.glob.translate(self.force_include, flags=flags)
self.force_include_re = ([ re.compile(x) for x in a ],
[ re.compile(x) for x in b ])
def match_re(self,
re: typing.Tuple[typing.List[typing.Pattern],
typing.List[typing.Pattern]],
path: bytes):
def match_re(self, r: MatchPatterns, path: bytes):
# Path matches if it matches at least one regex in
# re [0] and no regex in re [1].
for a in re [0]:
# r[0] and no regex in r[1].
for a in r[0]:
if a.match(path):
for b in re [1]:
for b in r[1]:
if b.match(path):
return False
return True
@@ -166,7 +162,7 @@ class Backup:
# See if there's a reason to exclude it
exclude_reason = None
if self.config.match_re(self.config.exclude_re , decorated_path):
if self.config.match_re(self.config.exclude, decorated_path):
# Config file says to exclude
exclude_reason = ('I', f"skipping, excluded by config file")
@@ -187,8 +183,7 @@ class Backup:
# If we have a reason to exclude it, stop now unless it's
# force-included
force = self.config.match_re(self.config.force_include_re,
decorated_path)
force = self.config.match_re(self.config.unexclude, decorated_path)
if exclude_reason and not force:
self.log(exclude_reason[0],
f"{exclude_reason[1]}: {pstr(path)}")