create.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. import logging
  2. import borgmatic.actions.json
  3. import borgmatic.borg.create
  4. import borgmatic.borg.feature
  5. import borgmatic.borg.rename
  6. import borgmatic.borg.repo_list
  7. import borgmatic.config.paths
  8. import borgmatic.config.validate
  9. import borgmatic.hooks.dispatch
  10. from borgmatic.actions import pattern
  11. logger = logging.getLogger(__name__)
  12. def run_create(
  13. config_filename,
  14. repository,
  15. config,
  16. config_paths,
  17. local_borg_version,
  18. create_arguments,
  19. global_arguments,
  20. dry_run_label,
  21. local_path,
  22. remote_path,
  23. ):
  24. '''
  25. Run the "create" action for the given repository.
  26. If create_arguments.json is True, yield the JSON output from creating the archive.
  27. '''
  28. if create_arguments.repository and not borgmatic.config.validate.repositories_match(
  29. repository,
  30. create_arguments.repository,
  31. ):
  32. return
  33. if config.get('list_details') and config.get('progress'):
  34. raise ValueError(
  35. 'With the create action, only one of --list/--files/list_details and --progress/progress can be used.',
  36. )
  37. if config.get('list_details') and create_arguments.json:
  38. raise ValueError(
  39. 'With the create action, only one of --list/--files/list_details and --json can be used.',
  40. )
  41. logger.info(f'Creating archive{dry_run_label}')
  42. working_directory = borgmatic.config.paths.get_working_directory(config)
  43. with borgmatic.config.paths.Runtime_directory(config) as borgmatic_runtime_directory:
  44. borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
  45. 'remove_data_source_dumps',
  46. config,
  47. borgmatic.hooks.dispatch.Hook_type.DATA_SOURCE,
  48. borgmatic_runtime_directory,
  49. global_arguments.dry_run,
  50. )
  51. patterns = pattern.process_patterns(
  52. pattern.collect_patterns(config),
  53. config,
  54. working_directory,
  55. )
  56. active_dumps = borgmatic.hooks.dispatch.call_hooks(
  57. 'dump_data_sources',
  58. config,
  59. borgmatic.hooks.dispatch.Hook_type.DATA_SOURCE,
  60. config_paths,
  61. borgmatic_runtime_directory,
  62. patterns,
  63. global_arguments.dry_run,
  64. )
  65. # Process the patterns again in case any data source hooks updated them. Without this step,
  66. # we could end up with duplicate paths that cause Borg to hang when it tries to read from
  67. # the same named pipe twice.
  68. patterns = pattern.process_patterns(
  69. patterns,
  70. config,
  71. working_directory,
  72. skip_expand_paths=config_paths,
  73. )
  74. stream_processes = [process for processes in active_dumps.values() for process in processes]
  75. # If we have stream processes, we first create an archive with .checkpoint suffix. This is
  76. # to make sure we only create a real archive if all the streaming processes completed
  77. # successfully (create_archive will fail if a streaming process fails, but the archive might
  78. # have already been created at this point).
  79. use_checkpoint = bool(stream_processes)
  80. json_output = borgmatic.borg.create.create_archive(
  81. global_arguments.dry_run,
  82. repository['path'],
  83. config,
  84. patterns,
  85. local_borg_version,
  86. global_arguments,
  87. borgmatic_runtime_directory,
  88. archive_suffix='.checkpoint' if use_checkpoint else '',
  89. local_path=local_path,
  90. remote_path=remote_path,
  91. json=create_arguments.json,
  92. comment=create_arguments.comment,
  93. stream_processes=stream_processes,
  94. )
  95. if use_checkpoint:
  96. rename_checkpoint_archive(
  97. repository['path'],
  98. global_arguments,
  99. config,
  100. local_borg_version,
  101. local_path,
  102. remote_path,
  103. )
  104. if json_output:
  105. output = borgmatic.actions.json.parse_json(json_output, repository.get('label'))
  106. if use_checkpoint:
  107. # Patch archive name and ID
  108. renamed_archive = borgmatic.borg.repo_list.get_latest_archive(
  109. repository['path'],
  110. config,
  111. local_borg_version,
  112. global_arguments,
  113. local_path,
  114. remote_path,
  115. )
  116. output['archive']['name'] = renamed_archive['name']
  117. output['archive']['id'] = renamed_archive['id']
  118. yield output
  119. borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
  120. 'remove_data_source_dumps',
  121. config,
  122. borgmatic.hooks.dispatch.Hook_type.DATA_SOURCE,
  123. borgmatic_runtime_directory,
  124. global_arguments.dry_run,
  125. )
  126. def rename_checkpoint_archive(
  127. repository_path,
  128. global_arguments,
  129. config,
  130. local_borg_version,
  131. local_path,
  132. remote_path,
  133. ):
  134. '''
  135. Renames the latest archive to not have a '.checkpoint' suffix.
  136. Raises ValueError if
  137. - there is not latest archive
  138. - the latest archive does not have a '.checkpoint' suffix
  139. Implementation note: We cannot reliably get the just created archive name.
  140. So we resort to listing the archives and picking the last one.
  141. A similar comment applies to retrieving the ID of the renamed archive.
  142. '''
  143. archive = borgmatic.borg.repo_list.get_latest_archive(
  144. repository_path,
  145. config,
  146. local_borg_version,
  147. global_arguments,
  148. local_path,
  149. remote_path,
  150. consider_checkpoints=True,
  151. )
  152. archive_name = archive['name']
  153. if not archive_name.endswith('.checkpoint'):
  154. raise ValueError(f'Latest archive did not have a .checkpoint suffix. Got: {archive_name}')
  155. new_archive_name = archive_name.removesuffix('.checkpoint')
  156. logger.info(f'Renaming archive {archive_name} -> {new_archive_name}')
  157. borgmatic.borg.rename.rename_archive(
  158. repository_path,
  159. (
  160. archive['id']
  161. if borgmatic.borg.feature.available(
  162. borgmatic.borg.feature.Feature.ARCHIVE_SERIES, local_borg_version
  163. )
  164. else archive['name']
  165. ),
  166. new_archive_name,
  167. global_arguments.dry_run,
  168. config,
  169. local_borg_version,
  170. local_path,
  171. remote_path,
  172. )