A wrapper script for Borg backup software that creates and prunes backups
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

147 lines
5.5KB

  1. import glob
  2. import itertools
  3. import logging
  4. import os
  5. import subprocess
  6. import tempfile
  7. logger = logging.getLogger(__name__)
  8. def _expand_directory(directory):
  9. '''
  10. Given a directory path, expand any tilde (representing a user's home directory) and any globs
  11. therein. Return a list of one or more resulting paths.
  12. '''
  13. expanded_directory = os.path.expanduser(directory)
  14. return glob.glob(expanded_directory) or [expanded_directory]
  15. def _expand_directories(directories):
  16. '''
  17. Given a sequence of directory paths, expand tildes and globs in each one. Return all the
  18. resulting directories as a single flattened tuple.
  19. '''
  20. if directories is None:
  21. return ()
  22. return tuple(
  23. itertools.chain.from_iterable(_expand_directory(directory) for directory in directories)
  24. )
  25. def _write_pattern_file(patterns=None):
  26. '''
  27. Given a sequence of patterns, write them to a named temporary file and return it. Return None
  28. if no patterns are provided.
  29. '''
  30. if not patterns:
  31. return None
  32. pattern_file = tempfile.NamedTemporaryFile('w')
  33. pattern_file.write('\n'.join(patterns))
  34. pattern_file.flush()
  35. return pattern_file
  36. def _make_pattern_flags(location_config, pattern_filename=None):
  37. '''
  38. Given a location config dict with a potential pattern_from option, and a filename containing any
  39. additional patterns, return the corresponding Borg flags for those files as a tuple.
  40. '''
  41. pattern_filenames = tuple(location_config.get('patterns_from') or ()) + (
  42. (pattern_filename,) if pattern_filename else ()
  43. )
  44. return tuple(
  45. itertools.chain.from_iterable(
  46. ('--patterns-from', pattern_filename) for pattern_filename in pattern_filenames
  47. )
  48. )
  49. def _make_exclude_flags(location_config, exclude_filename=None):
  50. '''
  51. Given a location config dict with various exclude options, and a filename containing any exclude
  52. patterns, return the corresponding Borg flags as a tuple.
  53. '''
  54. exclude_filenames = tuple(location_config.get('exclude_from') or ()) + (
  55. (exclude_filename,) if exclude_filename else ()
  56. )
  57. exclude_from_flags = tuple(
  58. itertools.chain.from_iterable(
  59. ('--exclude-from', exclude_filename) for exclude_filename in exclude_filenames
  60. )
  61. )
  62. caches_flag = ('--exclude-caches',) if location_config.get('exclude_caches') else ()
  63. if_present = location_config.get('exclude_if_present')
  64. if_present_flags = ('--exclude-if-present', if_present) if if_present else ()
  65. return exclude_from_flags + caches_flag + if_present_flags
  66. def create_archive(
  67. dry_run,
  68. repository,
  69. location_config,
  70. storage_config,
  71. local_path='borg',
  72. remote_path=None,
  73. progress=False,
  74. json=False,
  75. ):
  76. '''
  77. Given vebosity/dry-run flags, a local or remote repository path, a location config dict, and a
  78. storage config dict, create a Borg archive.
  79. '''
  80. sources = _expand_directories(location_config['source_directories'])
  81. pattern_file = _write_pattern_file(location_config.get('patterns'))
  82. exclude_file = _write_pattern_file(_expand_directories(location_config.get('exclude_patterns')))
  83. checkpoint_interval = storage_config.get('checkpoint_interval', None)
  84. chunker_params = storage_config.get('chunker_params', None)
  85. compression = storage_config.get('compression', None)
  86. remote_rate_limit = storage_config.get('remote_rate_limit', None)
  87. umask = storage_config.get('umask', None)
  88. lock_wait = storage_config.get('lock_wait', None)
  89. files_cache = location_config.get('files_cache')
  90. default_archive_name_format = '{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}'
  91. archive_name_format = storage_config.get('archive_name_format', default_archive_name_format)
  92. full_command = (
  93. (
  94. local_path,
  95. 'create',
  96. '{repository}::{archive_name_format}'.format(
  97. repository=repository, archive_name_format=archive_name_format
  98. ),
  99. )
  100. + sources
  101. + _make_pattern_flags(location_config, pattern_file.name if pattern_file else None)
  102. + _make_exclude_flags(location_config, exclude_file.name if exclude_file else None)
  103. + (('--checkpoint-interval', str(checkpoint_interval)) if checkpoint_interval else ())
  104. + (('--chunker-params', chunker_params) if chunker_params else ())
  105. + (('--compression', compression) if compression else ())
  106. + (('--remote-ratelimit', str(remote_rate_limit)) if remote_rate_limit else ())
  107. + (('--one-file-system',) if location_config.get('one_file_system') else ())
  108. + (('--read-special',) if location_config.get('read_special') else ())
  109. + (('--nobsdflags',) if location_config.get('bsd_flags') is False else ())
  110. + (('--files-cache', files_cache) if files_cache else ())
  111. + (('--remote-path', remote_path) if remote_path else ())
  112. + (('--umask', str(umask)) if umask else ())
  113. + (('--lock-wait', str(lock_wait)) if lock_wait else ())
  114. + (('--list', '--filter', 'AME-') if logger.isEnabledFor(logging.INFO) else ())
  115. + (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
  116. + (('--stats',) if not dry_run and logger.isEnabledFor(logging.INFO) else ())
  117. + (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
  118. + (('--dry-run',) if dry_run else ())
  119. + (('--progress',) if progress else ())
  120. + (('--json',) if json else ())
  121. )
  122. logger.debug(' '.join(full_command))
  123. subprocess.check_call(full_command)