__init__.py 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. import datetime
  2. import re
  3. import subprocess
  4. import sys
  5. import time
  6. class DuplicityCommandFailedError(RuntimeError):
  7. @property
  8. def msg(self):
  9. return self.__cause__.output.decode(sys.stdout.encoding)
  10. def _duplicity(params, timeout_seconds=None):
  11. try:
  12. stdout = subprocess.check_output(
  13. ['duplicity']
  14. + (['--timeout', str(timeout_seconds)] if timeout_seconds else [])
  15. + params,
  16. stderr=subprocess.STDOUT,
  17. )
  18. except subprocess.CalledProcessError as ex:
  19. raise DuplicityCommandFailedError() from ex
  20. return stdout.decode(sys.stdout.encoding)
  21. def _parse_duplicity_timestamp(timestamp):
  22. return datetime.datetime.fromtimestamp(
  23. time.mktime(time.strptime(timestamp))
  24. )
  25. class Collection(object):
  26. def __init__(self, url):
  27. self.url = url
  28. def request_status(self, timeout_seconds=None):
  29. return _CollectionStatus._parse(
  30. text=_duplicity(
  31. ['collection-status', self.url],
  32. timeout_seconds=timeout_seconds,
  33. )
  34. )
  35. class _Status(object):
  36. def __eq__(self, other):
  37. return isinstance(self, type(other)) and vars(self) == vars(other)
  38. def __neq__(self, other):
  39. return not (self == other)
  40. class _CollectionStatus(_Status):
  41. chain_separator_regex = r'-{25}\s'
  42. def __init__(self, archive_dir_path, primary_chain):
  43. self.archive_dir_path = archive_dir_path
  44. self.primary_chain = primary_chain
  45. @property
  46. def last_full_backup_time(self):
  47. return self.primary_chain.first_backup_time if self.primary_chain else None
  48. @property
  49. def last_incremental_backup_time(self):
  50. return self.primary_chain.last_incremental_backup_time if self.primary_chain else None
  51. @classmethod
  52. def _parse(cls, text):
  53. if 'No backup chains with active signatures found' in text:
  54. primary_chain = None
  55. else:
  56. primary_chain_match = re.search(
  57. '^Found primary backup chain.*\s{sep}([\w\W]*?)\s{sep}'.format(
  58. sep=_CollectionStatus.chain_separator_regex,
  59. ),
  60. text,
  61. re.MULTILINE,
  62. )
  63. primary_chain = _ChainStatus._parse(
  64. text=primary_chain_match.group(1),
  65. )
  66. return cls(
  67. archive_dir_path=re.search(r'Archive dir: (.*)', text).group(1),
  68. primary_chain=primary_chain,
  69. )
  70. class _ChainStatus(_Status):
  71. def __init__(self, sets):
  72. self.sets = sets
  73. @property
  74. def first_backup_time(self):
  75. return min([s.backup_time for s in self.sets])
  76. @property
  77. def last_backup_time(self):
  78. return max([s.backup_time for s in self.sets])
  79. @property
  80. def last_incremental_backup_time(self):
  81. return self.last_backup_time if len(self.sets) > 1 else None
  82. @classmethod
  83. def _parse(cls, text):
  84. sets = []
  85. set_lines = re.split(r'Num volumes: *\r?\n', text)[1]
  86. for set_line in re.split(r'\r?\n', set_lines):
  87. set_attr = re.match(
  88. r'\s*(?P<mode>\w+) {2,}(?P<ts>.+?) {2,} (?P<vol>\d+)',
  89. set_line,
  90. ).groupdict()
  91. # duplicity uses time.asctime().
  92. # time.strptime() without format inverts time.asctime().
  93. sets.append(_SetStatus(
  94. backup_time=_parse_duplicity_timestamp(set_attr['ts']),
  95. ))
  96. return cls(sets=sets)
  97. class _SetStatus(_Status):
  98. def __init__(self, backup_time):
  99. self.backup_time = backup_time