create.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. import glob
  2. import itertools
  3. import logging
  4. import os
  5. import tempfile
  6. from borgmatic.execute import execute_command, execute_command_without_capture
  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 _expand_home_directories(directories):
  26. '''
  27. Given a sequence of directory paths, expand tildes in each one. Do not perform any globbing.
  28. Return the results as a tuple.
  29. '''
  30. if directories is None:
  31. return ()
  32. return tuple(os.path.expanduser(directory) for directory in directories)
  33. def _write_pattern_file(patterns=None):
  34. '''
  35. Given a sequence of patterns, write them to a named temporary file and return it. Return None
  36. if no patterns are provided.
  37. '''
  38. if not patterns:
  39. return None
  40. pattern_file = tempfile.NamedTemporaryFile('w')
  41. pattern_file.write('\n'.join(patterns))
  42. pattern_file.flush()
  43. return pattern_file
  44. def _make_pattern_flags(location_config, pattern_filename=None):
  45. '''
  46. Given a location config dict with a potential patterns_from option, and a filename containing
  47. any additional patterns, return the corresponding Borg flags for those files as a tuple.
  48. '''
  49. pattern_filenames = tuple(location_config.get('patterns_from') or ()) + (
  50. (pattern_filename,) if pattern_filename else ()
  51. )
  52. return tuple(
  53. itertools.chain.from_iterable(
  54. ('--patterns-from', pattern_filename) for pattern_filename in pattern_filenames
  55. )
  56. )
  57. def _make_exclude_flags(location_config, exclude_filename=None):
  58. '''
  59. Given a location config dict with various exclude options, and a filename containing any exclude
  60. patterns, return the corresponding Borg flags as a tuple.
  61. '''
  62. exclude_filenames = tuple(location_config.get('exclude_from') or ()) + (
  63. (exclude_filename,) if exclude_filename else ()
  64. )
  65. exclude_from_flags = tuple(
  66. itertools.chain.from_iterable(
  67. ('--exclude-from', exclude_filename) for exclude_filename in exclude_filenames
  68. )
  69. )
  70. caches_flag = ('--exclude-caches',) if location_config.get('exclude_caches') else ()
  71. if_present = location_config.get('exclude_if_present')
  72. if_present_flags = ('--exclude-if-present', if_present) if if_present else ()
  73. keep_exclude_tags_flags = (
  74. ('--keep-exclude-tags',) if location_config.get('keep_exclude_tags') else ()
  75. )
  76. exclude_nodump_flags = ('--exclude-nodump',) if location_config.get('exclude_nodump') else ()
  77. return (
  78. exclude_from_flags
  79. + caches_flag
  80. + if_present_flags
  81. + keep_exclude_tags_flags
  82. + exclude_nodump_flags
  83. )
  84. DEFAULT_BORGMATIC_SOURCE_DIRECTORY = '~/.borgmatic'
  85. def borgmatic_source_directories(borgmatic_source_directory):
  86. '''
  87. Return a list of borgmatic-specific source directories used for state like database backups.
  88. '''
  89. if not borgmatic_source_directory:
  90. borgmatic_source_directory = DEFAULT_BORGMATIC_SOURCE_DIRECTORY
  91. return (
  92. [borgmatic_source_directory]
  93. if os.path.exists(os.path.expanduser(borgmatic_source_directory))
  94. else []
  95. )
  96. def create_archive(
  97. dry_run,
  98. repository,
  99. location_config,
  100. storage_config,
  101. local_path='borg',
  102. remote_path=None,
  103. progress=False,
  104. stats=False,
  105. json=False,
  106. files=False,
  107. ):
  108. '''
  109. Given vebosity/dry-run flags, a local or remote repository path, a location config dict, and a
  110. storage config dict, create a Borg archive and return Borg's JSON output (if any).
  111. '''
  112. sources = _expand_directories(
  113. location_config['source_directories']
  114. + borgmatic_source_directories(location_config.get('borgmatic_source_directory'))
  115. )
  116. pattern_file = _write_pattern_file(location_config.get('patterns'))
  117. exclude_file = _write_pattern_file(
  118. _expand_home_directories(location_config.get('exclude_patterns'))
  119. )
  120. checkpoint_interval = storage_config.get('checkpoint_interval', None)
  121. chunker_params = storage_config.get('chunker_params', None)
  122. compression = storage_config.get('compression', None)
  123. remote_rate_limit = storage_config.get('remote_rate_limit', None)
  124. umask = storage_config.get('umask', None)
  125. lock_wait = storage_config.get('lock_wait', None)
  126. files_cache = location_config.get('files_cache')
  127. default_archive_name_format = '{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}'
  128. archive_name_format = storage_config.get('archive_name_format', default_archive_name_format)
  129. extra_borg_options = storage_config.get('extra_borg_options', {}).get('create', '')
  130. full_command = (
  131. (local_path, 'create')
  132. + _make_pattern_flags(location_config, pattern_file.name if pattern_file else None)
  133. + _make_exclude_flags(location_config, exclude_file.name if exclude_file else None)
  134. + (('--checkpoint-interval', str(checkpoint_interval)) if checkpoint_interval else ())
  135. + (('--chunker-params', chunker_params) if chunker_params else ())
  136. + (('--compression', compression) if compression else ())
  137. + (('--remote-ratelimit', str(remote_rate_limit)) if remote_rate_limit else ())
  138. + (('--one-file-system',) if location_config.get('one_file_system') else ())
  139. + (('--numeric-owner',) if location_config.get('numeric_owner') else ())
  140. + (('--noatime',) if location_config.get('atime') is False else ())
  141. + (('--noctime',) if location_config.get('ctime') is False else ())
  142. + (('--nobirthtime',) if location_config.get('birthtime') is False else ())
  143. + (('--read-special',) if location_config.get('read_special') else ())
  144. + (('--nobsdflags',) if location_config.get('bsd_flags') is False else ())
  145. + (('--files-cache', files_cache) if files_cache else ())
  146. + (('--remote-path', remote_path) if remote_path else ())
  147. + (('--umask', str(umask)) if umask else ())
  148. + (('--lock-wait', str(lock_wait)) if lock_wait else ())
  149. + (
  150. ('--list', '--filter', 'AME-')
  151. if (files or logger.isEnabledFor(logging.DEBUG)) and not json and not progress
  152. else ()
  153. )
  154. + (('--info',) if logger.getEffectiveLevel() == logging.INFO and not json else ())
  155. + (
  156. ('--stats',)
  157. if (stats or logger.isEnabledFor(logging.DEBUG)) and not json and not dry_run
  158. else ()
  159. )
  160. + (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) and not json else ())
  161. + (('--dry-run',) if dry_run else ())
  162. + (('--progress',) if progress else ())
  163. + (('--json',) if json else ())
  164. + (tuple(extra_borg_options.split(' ')) if extra_borg_options else ())
  165. + (
  166. '{repository}::{archive_name_format}'.format(
  167. repository=repository, archive_name_format=archive_name_format
  168. ),
  169. )
  170. + sources
  171. )
  172. # The progress output isn't compatible with captured and logged output, as progress messes with
  173. # the terminal directly.
  174. if progress:
  175. execute_command_without_capture(full_command, error_on_warnings=False)
  176. return
  177. if json:
  178. output_log_level = None
  179. else:
  180. output_log_level = logging.INFO
  181. return execute_command(full_command, output_log_level, error_on_warnings=False)