# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright 2002 Ben Escoto <ben@emerose.org>
# Copyright 2007 Kenneth Loafman <kenneth@loafman.com>
#
# This file is part of duplicity.
#
# Duplicity is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or (at your
# option) any later version.
#
# Duplicity is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with duplicity; if not, write to the Free Software Foundation,
# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""Produce and parse the names of duplicity's backup files"""
import re
from duplicity import dup_time
from duplicity import globals
full_vol_re = None
full_vol_re_short = None
full_manifest_re = None
full_manifest_re_short = None
inc_vol_re = None
inc_vol_re_short = None
inc_manifest_re = None
inc_manifest_re_short = None
full_sig_re = None
full_sig_re_short = None
new_sig_re = None
new_sig_re_short = None
[docs]def prepare_regex(force=False):
global full_vol_re
global full_vol_re_short
global full_manifest_re
global full_manifest_re_short
global inc_vol_re
global inc_vol_re_short
global inc_manifest_re
global inc_manifest_re_short
global full_sig_re
global full_sig_re_short
global new_sig_re
global new_sig_re_short
# we force regex re-generation in unit tests because file prefixes might have changed
if full_vol_re and not force:
return
full_vol_re = re.compile("^" + globals.file_prefix + globals.file_prefix_archive + "duplicity-full"
"\\.(?P<time>.*?)"
"\\.vol(?P<num>[0-9]+)"
"\\.difftar"
"(?P<partial>(\\.part))?"
"($|\\.)")
full_vol_re_short = re.compile("^" + globals.file_prefix + globals.file_prefix_archive + "df"
"\\.(?P<time>[0-9a-z]+?)"
"\\.(?P<num>[0-9a-z]+)"
"\\.dt"
"(?P<partial>(\\.p))?"
"($|\\.)")
full_manifest_re = re.compile("^" + globals.file_prefix + globals.file_prefix_manifest + "duplicity-full"
"\\.(?P<time>.*?)"
"\\.manifest"
"(?P<partial>(\\.part))?"
"($|\\.)")
full_manifest_re_short = re.compile("^" + globals.file_prefix + globals.file_prefix_manifest + "df"
"\\.(?P<time>[0-9a-z]+?)"
"\\.m"
"(?P<partial>(\\.p))?"
"($|\\.)")
inc_vol_re = re.compile("^" + globals.file_prefix + globals.file_prefix_archive + "duplicity-inc"
"\\.(?P<start_time>.*?)"
"\\.to\\.(?P<end_time>.*?)"
"\\.vol(?P<num>[0-9]+)"
"\\.difftar"
"($|\\.)")
inc_vol_re_short = re.compile("^" + globals.file_prefix + globals.file_prefix_archive + "di"
"\\.(?P<start_time>[0-9a-z]+?)"
"\\.(?P<end_time>[0-9a-z]+?)"
"\\.(?P<num>[0-9a-z]+)"
"\\.dt"
"($|\\.)")
inc_manifest_re = re.compile("^" + globals.file_prefix + globals.file_prefix_manifest + "duplicity-inc"
"\\.(?P<start_time>.*?)"
"\\.to"
"\\.(?P<end_time>.*?)"
"\\.manifest"
"(?P<partial>(\\.part))?"
"(\\.|$)")
inc_manifest_re_short = re.compile("^" + globals.file_prefix + globals.file_prefix_manifest + "di"
"\\.(?P<start_time>[0-9a-z]+?)"
"\\.(?P<end_time>[0-9a-z]+?)"
"\\.m"
"(?P<partial>(\\.p))?"
"(\\.|$)")
full_sig_re = re.compile("^" + globals.file_prefix + globals.file_prefix_signature + "duplicity-full-signatures"
"\\.(?P<time>.*?)"
"\\.sigtar"
"(?P<partial>(\\.part))?"
"(\\.|$)")
full_sig_re_short = re.compile("^" + globals.file_prefix + globals.file_prefix_signature + "dfs"
"\\.(?P<time>[0-9a-z]+?)"
"\\.st"
"(?P<partial>(\\.p))?"
"(\\.|$)")
new_sig_re = re.compile("^" + globals.file_prefix + globals.file_prefix_signature + "duplicity-new-signatures"
"\\.(?P<start_time>.*?)"
"\\.to"
"\\.(?P<end_time>.*?)"
"\\.sigtar"
"(?P<partial>(\\.part))?"
"(\\.|$)")
new_sig_re_short = re.compile("^" + globals.file_prefix + globals.file_prefix_signature + "dns"
"\\.(?P<start_time>[0-9a-z]+?)"
"\\.(?P<end_time>[0-9a-z]+?)"
"\\.st"
"(?P<partial>(\\.p))?"
"(\\.|$)")
[docs]def to_base36(n):
"""
Return string representation of n in base 36 (use 0-9 and a-z)
"""
div, mod = divmod(n, 36)
if mod <= 9:
last_digit = str(mod)
else:
last_digit = chr(ord('a') + mod - 10)
if n == mod:
return last_digit
else:
return to_base36(div) + last_digit
[docs]def from_base36(s):
"""
Convert string s in base 36 to long int
"""
total = 0
for i in range(len(s)):
total *= 36
digit_ord = ord(s[i])
if ord('0') <= digit_ord <= ord('9'):
total += digit_ord - ord('0')
elif ord('a') <= digit_ord <= ord('z'):
total += digit_ord - ord('a') + 10
else:
assert 0, "Digit %s in %s not in proper range" % (s[i], s)
return total
[docs]def get_suffix(encrypted, gzipped):
"""
Return appropriate suffix depending on status of
encryption, compression, and short_filenames.
"""
if encrypted:
gzipped = False
if encrypted:
if globals.short_filenames:
suffix = '.g'
else:
suffix = ".gpg"
elif gzipped:
if globals.short_filenames:
suffix = ".z"
else:
suffix = '.gz'
else:
suffix = ""
return suffix
[docs]def get(type, volume_number=None, manifest=False,
encrypted=False, gzipped=False, partial=False):
"""
Return duplicity filename of specified type
type can be "full", "inc", "full-sig", or "new-sig". volume_number
can be given with the full and inc types. If manifest is true the
filename is of a full or inc manifest file.
"""
assert dup_time.curtimestr
if encrypted:
gzipped = False
suffix = get_suffix(encrypted, gzipped)
part_string = ""
if globals.short_filenames:
if partial:
part_string = ".p"
else:
if partial:
part_string = ".part"
if type == "full-sig" or type == "new-sig":
assert not volume_number and not manifest
assert not (volume_number and part_string)
if type == "full-sig":
if globals.short_filenames:
return (globals.file_prefix + globals.file_prefix_signature +
"dfs.%s.st%s%s" %
(to_base36(dup_time.curtime), part_string, suffix))
else:
return (globals.file_prefix + globals.file_prefix_signature +
"duplicity-full-signatures.%s.sigtar%s%s" %
(dup_time.curtimestr, part_string, suffix))
elif type == "new-sig":
if globals.short_filenames:
return (globals.file_prefix + globals.file_prefix_signature +
"dns.%s.%s.st%s%s" %
(to_base36(dup_time.prevtime),
to_base36(dup_time.curtime),
part_string, suffix))
else:
return (globals.file_prefix + globals.file_prefix_signature +
"duplicity-new-signatures.%s.to.%s.sigtar%s%s" %
(dup_time.prevtimestr, dup_time.curtimestr,
part_string, suffix))
else:
assert volume_number or manifest
assert not (volume_number and manifest)
prefix = globals.file_prefix
if volume_number:
if globals.short_filenames:
vol_string = "%s.dt" % to_base36(volume_number)
else:
vol_string = "vol%d.difftar" % volume_number
prefix += globals.file_prefix_archive
else:
if globals.short_filenames:
vol_string = "m"
else:
vol_string = "manifest"
prefix += globals.file_prefix_manifest
if type == "full":
if globals.short_filenames:
return ("%sdf.%s.%s%s%s" % (prefix, to_base36(dup_time.curtime),
vol_string, part_string, suffix))
else:
return ("%sduplicity-full.%s.%s%s%s" % (prefix, dup_time.curtimestr,
vol_string, part_string, suffix))
elif type == "inc":
if globals.short_filenames:
return ("%sdi.%s.%s.%s%s%s" % (prefix, to_base36(dup_time.prevtime),
to_base36(dup_time.curtime),
vol_string, part_string, suffix))
else:
return ("%sduplicity-inc.%s.to.%s.%s%s%s" % (prefix, dup_time.prevtimestr,
dup_time.curtimestr,
vol_string, part_string, suffix))
else:
assert 0
[docs]def parse(filename):
"""
Parse duplicity filename, return None or ParseResults object
"""
def str2time(timestr, short):
"""
Return time in seconds if string can be converted, None otherwise
"""
if short:
t = from_base36(timestr)
else:
try:
t = dup_time.genstrtotime(timestr.upper())
except dup_time.TimeException:
return None
return t
def get_vol_num(s, short):
"""
Return volume number from volume number string
"""
if short:
return from_base36(s)
else:
return int(s)
def check_full():
"""
Return ParseResults if file is from full backup, None otherwise
"""
prepare_regex()
short = True
m1 = full_vol_re_short.search(filename)
m2 = full_manifest_re_short.search(filename)
if not m1 and not m2 and not globals.short_filenames:
short = False
m1 = full_vol_re.search(filename)
m2 = full_manifest_re.search(filename)
if m1 or m2:
t = str2time((m1 or m2).group("time"), short)
if t:
if m1:
return ParseResults("full", time=t,
volume_number=get_vol_num(m1.group("num"), short))
else:
return ParseResults("full", time=t, manifest=True,
partial=(m2.group("partial") is not None))
return None
def check_inc():
"""
Return ParseResults if file is from inc backup, None otherwise
"""
prepare_regex()
short = True
m1 = inc_vol_re_short.search(filename)
m2 = inc_manifest_re_short.search(filename)
if not m1 and not m2 and not globals.short_filenames:
short = False
m1 = inc_vol_re.search(filename)
m2 = inc_manifest_re.search(filename)
if m1 or m2:
t1 = str2time((m1 or m2).group("start_time"), short)
t2 = str2time((m1 or m2).group("end_time"), short)
if t1 and t2:
if m1:
return ParseResults("inc", start_time=t1,
end_time=t2, volume_number=get_vol_num(m1.group("num"), short))
else:
return ParseResults("inc", start_time=t1, end_time=t2, manifest=1,
partial=(m2.group("partial") is not None))
return None
def check_sig():
"""
Return ParseResults if file is a signature, None otherwise
"""
prepare_regex()
short = True
m = full_sig_re_short.search(filename)
if not m and not globals.short_filenames:
short = False
m = full_sig_re.search(filename)
if m:
t = str2time(m.group("time"), short)
if t:
return ParseResults("full-sig", time=t,
partial=(m.group("partial") is not None))
else:
return None
short = True
m = new_sig_re_short.search(filename)
if not m and not globals.short_filenames:
short = False
m = new_sig_re.search(filename)
if m:
t1 = str2time(m.group("start_time"), short)
t2 = str2time(m.group("end_time"), short)
if t1 and t2:
return ParseResults("new-sig", start_time=t1, end_time=t2,
partial=(m.group("partial") is not None))
return None
def set_encryption_or_compression(pr):
"""
Set encryption and compression flags in ParseResults pr
"""
if (filename.endswith('.z') or
not globals.short_filenames and filename.endswith('gz')):
pr.compressed = 1
else:
pr.compressed = None
if (filename.endswith('.g') or
not globals.short_filenames and filename.endswith('.gpg')):
pr.encrypted = 1
else:
pr.encrypted = None
pr = check_full()
if not pr:
pr = check_inc()
if not pr:
pr = check_sig()
if not pr:
return None
set_encryption_or_compression(pr)
return pr
[docs]class ParseResults:
"""
Hold information taken from a duplicity filename
"""
def __init__(self, type, manifest=None, volume_number=None,
time=None, start_time=None, end_time=None,
encrypted=None, compressed=None, partial=False):
assert type in ["full-sig", "new-sig", "inc", "full"]
self.type = type
if type == "inc" or type == "full":
assert manifest or volume_number
if type == "inc" or type == "new-sig":
assert start_time and end_time
else:
assert time
self.manifest = manifest
self.volume_number = volume_number
self.time = time
self.start_time, self.end_time = start_time, end_time
self.compressed = compressed # true if gzip compressed
self.encrypted = encrypted # true if gpg encrypted
self.partial = partial